diff --git a/CHANGELOG.md b/CHANGELOG.md index 0322f00..05f6c4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog - [Changelog](#changelog) + - [0.6.1](#061) - [0.6.0](#060) - [0.5.1](#051) - [0.5.0](#050) @@ -19,6 +20,25 @@ --- +## 0.6.1 + +Released on 31/08/2021 + +- Enhancements: + - Now that tui-rs supports title alignment, UI has been improved + - Added new `Directory already exists` variant for file transfer errors +- Bugfix: + - Fixed [Issue 58](https://github.com/veeso/termscp/issues/58):When uploading a directory, create directory only if it doesn't exist + - Fixed [Issue 59](https://github.com/veeso/termscp/issues/59): When copying files with tricky copy, the upper progress bar shows no text +- Dependencies: + - Updated `bitflags` to `1.3.2` + - Updated `bytesize` to `1.1.0` + - Updated `crossterm` to `0.20` + - Updated `open` to `2.0.1` + - Added `tui-realm-stdlib 0.6.0` + - Replaced `ftp4` with `suppaftp 4.1.2` + - Updated `tui-realm` to `0.6.0` + ## 0.6.0 Released on 23/07/2021 diff --git a/Cargo.lock b/Cargo.lock index 3dab2b9..0684892 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,9 +94,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" @@ -138,9 +138,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytesize" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a18687293a1546b67c246452202bbbf143d239cb43494cc163da14979082da" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" [[package]] name = "cassowary" @@ -150,9 +150,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" [[package]] name = "cfg-if" @@ -264,34 +264,34 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c" +checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d" dependencies = [ "bitflags", "crossterm_winapi", - "lazy_static", "libc", "mio", "parking_lot 0.11.1", "signal-hook", + "signal-hook-mio", "winapi", ] [[package]] name = "crossterm_winapi" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9" +checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507" dependencies = [ "winapi", ] [[package]] name = "crypto-mac" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" dependencies = [ "generic-array", "subtle", @@ -409,18 +409,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "ftp4" -version = "4.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e03634a7a0e74618f9adf1e088495caa54ea07e72d449813e6439ce8ac9906f" -dependencies = [ - "chrono", - "lazy_static", - "native-tls", - "regex", -] - [[package]] name = "generic-array" version = "0.14.4" @@ -506,9 +494,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if 1.0.0", ] @@ -521,9 +509,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "js-sys" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d" dependencies = [ "wasm-bindgen", ] @@ -548,9 +536,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.98" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" +checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" [[package]] name = "libssh2-sys" @@ -630,9 +618,9 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matches" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "md-5" @@ -647,9 +635,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "mio" @@ -675,9 +663,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" dependencies = [ "lazy_static", "libc", @@ -790,19 +778,19 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "open" -version = "1.7.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1711eb4b31ce4ad35b0f316d8dfba4fe5c7ad601c448446d84aae7a896627b20" +checksum = "b46b233de7d83bc167fe43ae2dda3b5b84e80e09cceba581e4decb958a4896bf" dependencies = [ - "which", + "pathdiff", "winapi", ] [[package]] name = "openssl" -version = "0.10.35" +version = "0.10.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" +checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -820,9 +808,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.65" +version = "0.9.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" dependencies = [ "autocfg", "cc", @@ -884,7 +872,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.9", + "redox_syscall 0.2.10", "smallvec", "winapi", ] @@ -895,6 +883,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cacbb3c4ff353b534a67fb8d7524d00229da4cb1dc8c79f4db96e375ab5b619" +[[package]] +name = "pathdiff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -927,9 +921,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" dependencies = [ "unicode-xid", ] @@ -1032,9 +1026,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -1046,7 +1040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ "getrandom 0.2.3", - "redox_syscall 0.2.9", + "redox_syscall 0.2.10", ] [[package]] @@ -1209,18 +1203,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.126" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" dependencies = [ "proc-macro2", "quote", @@ -1229,9 +1223,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" dependencies = [ "itoa", "ryu", @@ -1253,13 +1247,23 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.1.17" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" +checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" dependencies = [ "libc", "mio", - "signal-hook-registry", + "signal-hook", ] [[package]] @@ -1314,15 +1318,28 @@ dependencies = [ [[package]] name = "subtle" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "suppaftp" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a4d861acfdc117c6d373c3b743c534dbbbb2d782e7646b27439a7c5282ad6a" +dependencies = [ + "chrono", + "lazy_static", + "native-tls", + "regex", + "thiserror", +] [[package]] name = "syn" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" dependencies = [ "proc-macro2", "quote", @@ -1338,7 +1355,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "rand 0.8.4", - "redox_syscall 0.2.9", + "redox_syscall 0.2.10", "remove_dir_all", "winapi", ] @@ -1354,7 +1371,7 @@ dependencies = [ [[package]] name = "termscp" -version = "0.6.0" +version = "0.6.1" dependencies = [ "argh", "bitflags", @@ -1364,7 +1381,6 @@ dependencies = [ "crossterm", "dirs", "edit", - "ftp4", "hostname", "keyring", "lazy_static", @@ -1379,10 +1395,12 @@ dependencies = [ "serde", "simplelog", "ssh2", + "suppaftp", "tempfile", "textwrap", "thiserror", "toml", + "tui-realm-stdlib", "tuirealm", "ureq", "users", @@ -1445,9 +1463,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" dependencies = [ "tinyvec_macros", ] @@ -1469,9 +1487,9 @@ dependencies = [ [[package]] name = "tui" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "861d8f3ad314ede6219bcb2ab844054b1de279ee37a9bc38e3d606f9d3fb2a71" +checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23" dependencies = [ "bitflags", "cassowary", @@ -1481,15 +1499,24 @@ dependencies = [ ] [[package]] -name = "tuirealm" -version = "0.4.3" +name = "tui-realm-stdlib" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcbd06f2aa6a2424aaa245c10e8767fe3f0fee234ac8c144cb15eaf2ee37ce9" +checksum = "6bff91e1cdc741a7487d8cb20ac038e5ba926a0ec97b0f2ea918ac75640b9da5" +dependencies = [ + "textwrap", + "tuirealm", + "unicode-width", +] + +[[package]] +name = "tuirealm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634ad8e6a4b80ef032d31356b55964a995da5d05a9cf3a1bd134bae1ba7c197a" dependencies = [ "crossterm", - "textwrap", "tui", - "unicode-width", ] [[package]] @@ -1500,18 +1527,15 @@ checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" [[package]] name = "unicode-bidi" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" -dependencies = [ - "matches", -] +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" [[package]] name = "unicode-linebreak" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a31f45d18a3213b918019f78fe6a73a14ab896807f0aaf5622aa0684749455" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" dependencies = [ "regex", ] @@ -1615,9 +1639,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -1625,9 +1649,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041" dependencies = [ "bumpalo", "lazy_static", @@ -1640,9 +1664,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1650,9 +1674,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad" dependencies = [ "proc-macro2", "quote", @@ -1663,15 +1687,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" +checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" [[package]] name = "web-sys" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c" dependencies = [ "js-sys", "wasm-bindgen", @@ -1698,19 +1722,20 @@ dependencies = [ [[package]] name = "which" -version = "4.1.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe" +checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" dependencies = [ "either", + "lazy_static", "libc", ] [[package]] name = "whoami" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6" +checksum = "f7741161a40200a867c96dfa5574544efa4178cf4c8f770b62dd1cc0362d7ae1" dependencies = [ "wasm-bindgen", "web-sys", diff --git a/Cargo.toml b/Cargo.toml index de7ea32..7066e40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" name = "termscp" readme = "README.md" repository = "https://github.com/veeso/termscp" -version = "0.6.0" +version = "0.6.1" [package.metadata.rpm] package = "termscp" @@ -28,31 +28,32 @@ path = "src/main.rs" [dependencies] argh = "0.1.5" -bitflags = "1.2.1" -bytesize = "1.0.1" +bitflags = "1.3.2" +bytesize = "1.1.0" chrono = "0.4.19" content_inspector = "0.2.4" -crossterm = "0.19.0" +crossterm = "0.20" dirs = "3.0.1" edit = "0.1.3" -ftp4 = { version = "4.0.2", features = [ "secure" ] } hostname = "0.3.1" keyring = { version = "0.10.1", optional = true } lazy_static = "1.4.0" log = "0.4.14" magic-crypt = "3.1.7" -open = "1.7.0" +open = "2.0.1" rand = "0.8.4" regex = "1.5.4" rpassword = "5.0.1" serde = { version = "^1.0.0", features = [ "derive" ] } simplelog = "0.10.0" ssh2 = "0.9.0" +suppaftp = { version = "4.1.2", features = [ "secure" ] } tempfile = "3.1.0" textwrap = "0.14.2" thiserror = "^1.0.0" toml = "0.5.8" -tuirealm = { version = "0.4.3", features = [ "with-components" ] } +tui-realm-stdlib = "0.6.0" +tuirealm = "0.6.0" ureq = { version = "2.1.0", features = [ "json" ] } whoami = "1.1.1" wildmatch = "2.0.0" diff --git a/README.md b/README.md index f69dcf6..bf29840 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@

Developed by @veeso

-

Current version: 0.6.0 (23/07/2021)

+

Current version: 0.6.1 (31/08/2021)

-[![License: MIT](https://img.shields.io/badge/License-MIT-teal.svg)](https://opensource.org/licenses/MIT) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.6.0-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp) +[![License: MIT](https://img.shields.io/badge/License-MIT-teal.svg)](https://opensource.org/licenses/MIT) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.6.1-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp) [![Linux](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![MacOs](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Windows](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions) [![FreeBSD](https://github.com/veeso/termscp/workflows/FreeBSD/badge.svg)](https://github.com/veeso/termscp/actions) [![Coverage Status](https://coveralls.io/repos/github/veeso/termscp/badge.svg)](https://coveralls.io/github/veeso/termscp) @@ -122,13 +122,14 @@ Major termscp releases will now be seasonal, so expect 4 major updates during th Planned for *🍁 Autumn update 🍇*: -- **Configuration profile for bookmarks 📚**: Basically this feature adds the possibility to have a specific setup for a certain host, instead of having only one global configuration. - **Self-update ⬇️**: In order to increase users updating termscp, I want to provide the possibility to update termscp directly from application, when a new update is available. - **AWS S3 support 🪣**: I'll use `rust-s3` library to implement this. This is really big **Maybe** for the autumn update and might be moved to the Winter update. +- **Prompt before replacing files ☢️**: Possibility to configure whether a prompt should be displayed before replacing files. Planned for *❄️ Winter update ⛄*: - **SMB Support 🎉**: This will require a long time to be implemented, since I'm currently working on a Rust native SMB library, since I don't want to add new C-bindings. ~~Fear the 🦚~~ +- **Configuration profile for bookmarks 📚**: Basically this feature adds the possibility to have a specific setup for a certain host, instead of having only one global configuration. Along to new features, termscp developments is now focused on UX and performance improvements, so if you have any suggestion, feel free to open an issue. @@ -159,8 +160,8 @@ termscp is powered by these aweseome projects: - [keyring-rs](https://github.com/hwchen/keyring-rs) - [open-rs](https://github.com/Byron/open-rs) - [rpassword](https://github.com/conradkleinespel/rpassword) -- [rust-ftp](https://github.com/mattnenterprise/rust-ftp) - [ssh2-rs](https://github.com/alexcrichton/ssh2-rs) +- [suppaftp](https://github.com/veeso/suppaftp) - [textwrap](https://github.com/mgeisler/textwrap) - [tui-rs](https://github.com/fdehau/tui-rs) - [tui-realm](https://github.com/veeso/tui-realm) diff --git a/dist/pkgs/freebsd/manifest b/dist/pkgs/freebsd/manifest index a63fcb2..c4b0d18 100644 --- a/dist/pkgs/freebsd/manifest +++ b/dist/pkgs/freebsd/manifest @@ -1,5 +1,5 @@ name: "termscp" -version: 0.5.1 +version: 0.6.1 origin: veeso/termscp comment: "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP" desc: < &ft_params, + Some(ft_params) => ft_params, None => { error!("Failed to start FileTransferActivity: file transfer params is None"); return None; diff --git a/src/config/serialization.rs b/src/config/serialization.rs index fa8db24..eacd88f 100644 --- a/src/config/serialization.rs +++ b/src/config/serialization.rs @@ -44,13 +44,13 @@ pub struct SerializerError { #[derive(Error, Debug)] pub enum SerializerErrorKind { #[error("Operation failed")] - GenericError, + Generic, #[error("IO error")] - IoError, + Io, #[error("Serialization error")] - SerializationError, + Serialization, #[error("Syntax error")] - SyntaxError, + Syntax, } impl SerializerError { @@ -92,7 +92,7 @@ where Ok(dt) => dt, Err(err) => { return Err(SerializerError::new_ex( - SerializerErrorKind::SerializationError, + SerializerErrorKind::Serialization, err.to_string(), )) } @@ -102,7 +102,7 @@ where match writable.write_all(data.as_bytes()) { Ok(_) => Ok(()), Err(err) => Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, err.to_string(), )), } @@ -119,7 +119,7 @@ where let mut data: String = String::new(); if let Err(err) = readable.read_to_string(&mut data) { return Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, err.to_string(), )); } @@ -131,7 +131,7 @@ where Ok(deserialized) } Err(err) => Err(SerializerError::new_ex( - SerializerErrorKind::SyntaxError, + SerializerErrorKind::Syntax, err.to_string(), )), } @@ -154,11 +154,11 @@ mod tests { #[test] fn test_config_serialization_errors() { - let error: SerializerError = SerializerError::new(SerializerErrorKind::SyntaxError); + let error: SerializerError = SerializerError::new(SerializerErrorKind::Syntax); assert!(error.msg.is_none()); assert_eq!(format!("{}", error), String::from("Syntax error")); let error: SerializerError = - SerializerError::new_ex(SerializerErrorKind::SyntaxError, String::from("bad syntax")); + SerializerError::new_ex(SerializerErrorKind::Syntax, String::from("bad syntax")); assert!(error.msg.is_some()); assert_eq!( format!("{}", error), @@ -166,20 +166,17 @@ mod tests { ); // Fmt assert_eq!( - format!( - "{}", - SerializerError::new(SerializerErrorKind::GenericError) - ), + format!("{}", SerializerError::new(SerializerErrorKind::Generic)), String::from("Operation failed") ); assert_eq!( - format!("{}", SerializerError::new(SerializerErrorKind::IoError)), + format!("{}", SerializerError::new(SerializerErrorKind::Io)), String::from("IO error") ); assert_eq!( format!( "{}", - SerializerError::new(SerializerErrorKind::SerializationError) + SerializerError::new(SerializerErrorKind::Serialization) ), String::from("Serialization error") ); diff --git a/src/filetransfer/ftp_transfer.rs b/src/filetransfer/ftp_transfer.rs index 20088c7..b0f5b82 100644 --- a/src/filetransfer/ftp_transfer.rs +++ b/src/filetransfer/ftp_transfer.rs @@ -26,19 +26,21 @@ * SOFTWARE. */ use super::{FileTransfer, FileTransferError, FileTransferErrorType}; -use crate::fs::{FsDirectory, FsEntry, FsFile}; -use crate::utils::fmt::{fmt_time, shadow_password}; -use crate::utils::parser::{parse_datetime, parse_lstime}; +use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; +use crate::utils::fmt::shadow_password; +use crate::utils::path; // Includes -use ftp4::native_tls::TlsConnector; -use ftp4::{types::FileType, FtpStream}; -use regex::Regex; +use std::convert::TryFrom; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use std::time::SystemTime; -use std::{ - io::{Read, Write}, - ops::Range, +use std::time::UNIX_EPOCH; +use suppaftp::native_tls::TlsConnector; +use suppaftp::{ + list::{File, PosixPexQuery}, + status::FILE_UNAVAILABLE, + types::{FileType, Response}, + FtpError, FtpStream, }; /// ## FtpFileTransfer @@ -71,319 +73,103 @@ impl FtpFileTransfer { p.to_path_buf() } - /// ### parse_list_line + /// ### parse_list_lines /// - /// Parse a line of LIST command output and instantiates an FsEntry from it - fn parse_list_line(&mut self, path: &Path, line: &str) -> Result { - // Try to parse using UNIX syntax - match self.parse_unix_list_line(path, line) { - Ok(entry) => Ok(entry), - Err(_) => match self.parse_dos_list_line(path, line) { - // If UNIX parsing fails, try DOS - Ok(entry) => Ok(entry), - Err(_) => Err(()), - }, - } - } - - /// ### parse_unix_list_line - /// - /// Try to parse a "LIST" output command line in UNIX format. - /// Returns error if syntax is not UNIX compliant. - /// UNIX syntax has the following syntax: - /// {FILE_TYPE}{UNIX_PEX} {HARD_LINKS} {USER} {GROUP} {SIZE} {DATE} {FILENAME} - /// -rw-r--r-- 1 cvisintin staff 4968 27 Dic 10:46 CHANGELOG.md - fn parse_unix_list_line(&mut self, path: &Path, line: &str) -> Result { - // Prepare list regex - // NOTE: about this damn regex - lazy_static! { - static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap(); - } - debug!("Parsing LIST (UNIX) line: '{}'", line); - // Apply regex to result - match LS_RE.captures(line) { - // String matches regex - Some(metadata) => { - // NOTE: metadata fmt: (regex, file_type, permissions, link_count, uid, gid, filesize, mtime, filename) - // Expected 7 + 1 (8) values: + 1 cause regex is repeated at 0 - if metadata.len() < 8 { - return Err(()); - } - // Collect metadata - // Get if is directory and if is symlink - let (mut is_dir, is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str() - { - "-" => (false, false), - "l" => (false, true), - "d" => (true, false), - _ => return Err(()), // Ignore special files - }; - // Check string length (unix pex) - if metadata.get(2).unwrap().as_str().len() < 9 { - return Err(()); - } - - let pex = |range: Range| { - let mut count: u8 = 0; - for (i, c) in metadata.get(2).unwrap().as_str()[range].chars().enumerate() { - match c { - '-' => {} - _ => { - count += match i { - 0 => 4, - 1 => 2, - 2 => 1, - _ => 0, - } - } - } - } - count - }; - - // Get unix pex - let unix_pex = (pex(0..3), pex(3..6), pex(6..9)); - - // Parse mtime and convert to SystemTime - let mtime: SystemTime = match parse_lstime( - metadata.get(7).unwrap().as_str(), - "%b %d %Y", - "%b %d %H:%M", - ) { - Ok(t) => t, - Err(_) => SystemTime::UNIX_EPOCH, - }; - // Get uid - let uid: Option = match metadata.get(4).unwrap().as_str().parse::() { - Ok(uid) => Some(uid), - Err(_) => None, - }; - // Get gid - let gid: Option = match metadata.get(5).unwrap().as_str().parse::() { - Ok(gid) => Some(gid), - Err(_) => None, - }; - // Get filesize - let filesize: usize = metadata - .get(6) - .unwrap() - .as_str() - .parse::() - .unwrap_or(0); - // Split filename if required - let (file_name, symlink_path): (String, Option) = match is_symlink { - true => self.get_name_and_link(metadata.get(8).unwrap().as_str()), - false => (String::from(metadata.get(8).unwrap().as_str()), None), - }; - // Check if file_name is '.' or '..' - if file_name.as_str() == "." || file_name.as_str() == ".." { - debug!("File name is {}; ignoring entry", file_name); - return Err(()); - } - // Get symlink - let symlink: Option> = symlink_path.map(|p| { - Box::new(match p.to_string_lossy().ends_with('/') { - true => { - // NOTE: is_dir becomes true - is_dir = true; - FsEntry::Directory(FsDirectory { - name: p - .file_name() - .unwrap_or(&std::ffi::OsStr::new("")) - .to_string_lossy() - .to_string(), - abs_path: p.clone(), - last_change_time: mtime, - last_access_time: mtime, - creation_time: mtime, - readonly: false, - symlink: None, - user: uid, - group: gid, - unix_pex: Some(unix_pex), - }) - } - false => FsEntry::File(FsFile { - name: p - .file_name() - .unwrap_or(&std::ffi::OsStr::new("")) - .to_string_lossy() - .to_string(), - abs_path: p.clone(), - last_change_time: mtime, - last_access_time: mtime, - creation_time: mtime, - readonly: false, - symlink: None, - size: filesize, - ftype: p.extension().map(|s| String::from(s.to_string_lossy())), - user: uid, - group: gid, - unix_pex: Some(unix_pex), - }), - }) - }); - let mut abs_path: PathBuf = PathBuf::from(path); - abs_path.push(file_name.as_str()); - let abs_path: PathBuf = Self::resolve(abs_path.as_path()); - // get extension - let extension: Option = abs_path - .as_path() - .extension() - .map(|s| String::from(s.to_string_lossy())); - // Return - debug!("Follows LIST line '{}' attributes", line); - debug!("Is directory? {}", is_dir); - debug!("Is symlink? {}", is_symlink); - debug!("name: {}", file_name); - debug!("abs_path: {}", abs_path.display()); - debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("symlink: {:?}", symlink); - debug!("user: {:?}", uid); - debug!("group: {:?}", gid); - debug!("unix_pex: {:?}", unix_pex); - debug!("---------------------------------------"); - // Push to entries - Ok(match is_dir { + /// Parse all lines of LIST command output and instantiates a vector of FsEntry from it. + /// This function also converts from `suppaftp::list::File` to `FsEntry` + fn parse_list_lines(&mut self, path: &Path, lines: Vec) -> Vec { + // Iter and collect + lines + .into_iter() + .map(File::try_from) // Try to convert to file + .flatten() // Remove errors + .map(|x| { + let mut abs_path: PathBuf = path.to_path_buf(); + abs_path.push(x.name()); + match x.is_directory() { true => FsEntry::Directory(FsDirectory { - name: file_name, + name: x.name().to_string(), abs_path, - last_change_time: mtime, - last_access_time: mtime, - creation_time: mtime, - readonly: false, - symlink, - user: uid, - group: gid, - unix_pex: Some(unix_pex), + last_access_time: x.modified(), + last_change_time: x.modified(), + creation_time: x.modified(), + symlink: None, + user: x.uid(), + group: x.gid(), + unix_pex: Some(Self::query_unix_pex(&x)), }), false => FsEntry::File(FsFile { - name: file_name, + name: x.name().to_string(), + size: x.size(), + ftype: abs_path + .extension() + .map(|ext| String::from(ext.to_str().unwrap_or(""))), + last_access_time: x.modified(), + last_change_time: x.modified(), + creation_time: x.modified(), + user: x.uid(), + group: x.gid(), + symlink: Self::get_symlink_entry(path, x.symlink()), abs_path, - last_change_time: mtime, - last_access_time: mtime, - creation_time: mtime, - size: filesize, - ftype: extension, - readonly: false, - symlink, - user: uid, - group: gid, - unix_pex: Some(unix_pex), + unix_pex: Some(Self::query_unix_pex(&x)), }), - }) - } - None => Err(()), - } - } - - /// ### parse_dos_list_line - /// - /// Try to parse a "LIST" output command line in DOS format. - /// Returns error if syntax is not DOS compliant. - /// DOS syntax has the following syntax: - /// {DATE} {TIME} { | SIZE} {FILENAME} - /// 10-19-20 03:19PM pub - /// 04-08-14 03:09PM 403 readme.txt - fn parse_dos_list_line(&self, path: &Path, line: &str) -> Result { - // Prepare list regex - // NOTE: you won't find this regex on the internet. It seems I'm the only person in the world who needs this - lazy_static! { - static ref DOS_RE: Regex = Regex::new( - r#"^(\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}\s*[AP]M)\s+()?([\d,]*)\s+(.+)$"# - ) - .unwrap(); - } - debug!("Parsing LIST (DOS) line: '{}'", line); - // Apply regex to result - match DOS_RE.captures(line) { - // String matches regex - Some(metadata) => { - // NOTE: metadata fmt: (regex, date_time, is_dir?, file_size?, file_name) - // Expected 4 + 1 (5) values: + 1 cause regex is repeated at 0 - if metadata.len() < 5 { - return Err(()); } - // Parse date time - let time: SystemTime = - match parse_datetime(metadata.get(1).unwrap().as_str(), "%d-%m-%y %I:%M%p") { - Ok(t) => t, - Err(_) => SystemTime::UNIX_EPOCH, // Don't return error - }; - // Get if is a directory - let is_dir: bool = metadata.get(2).is_some(); - // Get file size - let file_size: usize = match is_dir { - true => 0, // If is directory, filesize is 0 - false => match metadata.get(3) { - // If is file, parse arg 3 - Some(val) => val.as_str().parse::().unwrap_or(0), - None => 0, // Should not happen - }, - }; - // Get file name - let file_name: String = String::from(metadata.get(4).unwrap().as_str()); - // Get absolute path - let mut abs_path: PathBuf = PathBuf::from(path); - abs_path.push(file_name.as_str()); - let abs_path: PathBuf = Self::resolve(abs_path.as_path()); - // Get extension - let extension: Option = abs_path - .as_path() - .extension() - .map(|s| String::from(s.to_string_lossy())); - debug!("Follows LIST line '{}' attributes", line); - debug!("Is directory? {}", is_dir); - debug!("name: {}", file_name); - debug!("abs_path: {}", abs_path.display()); - debug!("last_change_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S")); - debug!("last_access_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S")); - debug!("creation_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S")); - debug!("---------------------------------------"); - // Return entry - Ok(match is_dir { - true => FsEntry::Directory(FsDirectory { - name: file_name, - abs_path, - last_change_time: time, - last_access_time: time, - creation_time: time, - readonly: false, - symlink: None, - user: None, - group: None, - unix_pex: None, - }), - false => FsEntry::File(FsFile { - name: file_name, - abs_path, - last_change_time: time, - last_access_time: time, - creation_time: time, - size: file_size, - ftype: extension, - readonly: false, - symlink: None, - user: None, - group: None, - unix_pex: None, - }), - }) + }) + .collect() + } + + /// ### get_symlink_entry + /// + /// Get FsEntry from symlink + fn get_symlink_entry(wrkdir: &Path, link: Option<&Path>) -> Option> { + match link { + None => None, + Some(p) => { + // Make abs path + let abs_path: PathBuf = path::absolutize(wrkdir, p); + Some(Box::new(FsEntry::File(FsFile { + name: p + .file_name() + .map(|x| x.to_str().unwrap_or("").to_string()) + .unwrap_or_default(), + ftype: abs_path + .extension() + .map(|ext| String::from(ext.to_str().unwrap_or(""))), + size: 0, + last_access_time: UNIX_EPOCH, + last_change_time: UNIX_EPOCH, + creation_time: UNIX_EPOCH, + user: None, + group: None, + symlink: None, + unix_pex: None, + abs_path, + }))) } - None => Err(()), // Invalid syntax } } - /// ### get_name_and_link + /// ### query_unix_pex /// - /// Returns from a `ls -l` command output file name token, the name of the file and the symbolic link (if there is any) - fn get_name_and_link(&self, token: &str) -> (String, Option) { - let tokens: Vec<&str> = token.split(" -> ").collect(); - let filename: String = String::from(*tokens.get(0).unwrap()); - let symlink: Option = tokens.get(1).map(PathBuf::from); - (filename, symlink) + /// Returns unix pex in tuple of values + fn query_unix_pex(f: &File) -> (UnixPex, UnixPex, UnixPex) { + ( + UnixPex::new( + f.can_read(PosixPexQuery::Owner), + f.can_write(PosixPexQuery::Owner), + f.can_execute(PosixPexQuery::Owner), + ), + UnixPex::new( + f.can_read(PosixPexQuery::Group), + f.can_write(PosixPexQuery::Group), + f.can_execute(PosixPexQuery::Group), + ), + UnixPex::new( + f.can_read(PosixPexQuery::Others), + f.can_write(PosixPexQuery::Others), + f.can_execute(PosixPexQuery::Others), + ), + ) } } @@ -473,7 +259,12 @@ impl FileTransfer for FtpFileTransfer { self.stream = Some(stream); info!("Connection successfully established"); // Return OK - Ok(self.stream.as_ref().unwrap().get_welcome_msg()) + Ok(self + .stream + .as_ref() + .unwrap() + .get_welcome_msg() + .map(|x| x.to_string())) } /// ### disconnect @@ -567,22 +358,10 @@ impl FileTransfer for FtpFileTransfer { info!("LIST dir {}", dir.display()); match &mut self.stream { Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) { - Ok(entries) => { - debug!("Got {} lines in LIST result", entries.len()); - // Prepare result - let mut result: Vec = Vec::with_capacity(entries.len()); + Ok(lines) => { + debug!("Got {} lines in LIST result", lines.len()); // Iterate over entries - for entry in entries.iter() { - if let Ok(file) = self.parse_list_line(dir.as_path(), entry) { - result.push(file); - } - } - debug!( - "{} out of {} were valid entries", - result.len(), - entries.len() - ); - Ok(result) + Ok(self.parse_list_lines(path, lines)) } Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::DirStatFailed, @@ -597,13 +376,23 @@ impl FileTransfer for FtpFileTransfer { /// ### mkdir /// - /// Make directory + /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> { let dir: PathBuf = Self::resolve(dir); info!("MKDIR {}", dir.display()); match &mut self.stream { Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) { Ok(_) => Ok(()), + Err(FtpError::UnexpectedResponse(Response { + // Directory already exists + code: FILE_UNAVAILABLE, + body: _, + })) => { + error!("Directory {} already exists", dir.display()); + Err(FileTransferError::new( + FileTransferErrorType::DirectoryAlreadyExists, + )) + } Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::FileCreateDenied, err.to_string(), @@ -659,7 +448,7 @@ impl FileTransfer for FtpFileTransfer { // Remove recursively files debug!("Removing {} entries from directory...", files.len()); for file in files.iter() { - if let Err(err) = self.remove(&file) { + if let Err(err) = self.remove(file) { return Err(FileTransferError::new_ex( FileTransferErrorType::PexError, err.to_string(), @@ -791,7 +580,8 @@ impl FileTransfer for FtpFileTransfer { fn recv_file(&mut self, file: &FsFile) -> Result, FileTransferError> { info!("Receiving file {}", file.abs_path.display()); match &mut self.stream { - Some(stream) => match stream.get(&file.abs_path.as_path().to_string_lossy()) { + Some(stream) => match stream.retr_as_stream(&file.abs_path.as_path().to_string_lossy()) + { Ok(reader) => Ok(Box::new(reader)), // NOTE: don't use BufReader here, since already returned by the library Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::NoSuchFileOrDirectory, @@ -837,7 +627,7 @@ impl FileTransfer for FtpFileTransfer { fn on_recv(&mut self, readable: Box) -> Result<(), FileTransferError> { info!("Finalizing get"); match &mut self.stream { - Some(stream) => match stream.finalize_get(readable) { + Some(stream) => match stream.finalize_retr_stream(readable) { Ok(_) => Ok(()), Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::ProtocolError, @@ -856,7 +646,6 @@ mod tests { use super::*; use crate::utils::file::open_file; - use crate::utils::fmt::fmt_time; #[cfg(feature = "with-containers")] use crate::utils::test_helpers::write_file; use crate::utils::test_helpers::{create_sample_file_entry, make_fsentry}; @@ -902,6 +691,14 @@ mod tests { assert_eq!(ftp.list_dir(&Path::new("/")).unwrap().len(), 0); // Make directory assert!(ftp.mkdir(PathBuf::from("/home").as_path()).is_ok()); + // Remake directory (should report already exists) + assert_eq!( + ftp.mkdir(PathBuf::from("/home").as_path()) + .err() + .unwrap() + .kind(), + FileTransferErrorType::DirectoryAlreadyExists + ); // Make directory (err) assert!(ftp.mkdir(PathBuf::from("/root/pommlar").as_path()).is_err()); // Change directory @@ -957,16 +754,15 @@ mod tests { let dummy: FsEntry = FsEntry::File(FsFile { name: String::from("cucumber.txt"), abs_path: PathBuf::from("/cucumber.txt"), - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, + last_change_time: UNIX_EPOCH, + last_access_time: UNIX_EPOCH, + creation_time: UNIX_EPOCH, size: 0, ftype: Some(String::from("txt")), // File type - readonly: true, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); assert!(ftp .rename(&dummy, PathBuf::from("/a/b/c").as_path()) @@ -1051,12 +847,13 @@ mod tests { let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); // Simple file let file: FsFile = ftp - .parse_list_line( + .parse_list_lines( PathBuf::from("/tmp").as_path(), - "-rw-rw-r-- 1 root dialout 8192 Nov 5 2018 omar.txt", + vec!["-rw-rw-r-- 1 root dialout 8192 Nov 5 2018 omar.txt".to_string()], ) - .ok() + .get(0) .unwrap() + .clone() .unwrap_file(); assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt")); assert_eq!(file.name, String::from("omar.txt")); @@ -1064,183 +861,28 @@ mod tests { assert!(file.symlink.is_none()); assert_eq!(file.user, None); assert_eq!(file.group, None); - assert_eq!(file.unix_pex.unwrap(), (6, 6, 4)); + assert_eq!( + file.unix_pex.unwrap(), + (UnixPex::from(6), UnixPex::from(6), UnixPex::from(4)) + ); assert_eq!( file.last_access_time - .duration_since(SystemTime::UNIX_EPOCH) + .duration_since(UNIX_EPOCH) .ok() .unwrap(), Duration::from_secs(1541376000) ); assert_eq!( file.last_change_time - .duration_since(SystemTime::UNIX_EPOCH) + .duration_since(UNIX_EPOCH) .ok() .unwrap(), Duration::from_secs(1541376000) ); assert_eq!( - file.creation_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), + file.creation_time.duration_since(UNIX_EPOCH).ok().unwrap(), Duration::from_secs(1541376000) ); - // Simple file with number as gid, uid - let file: FsFile = ftp - .parse_list_line( - PathBuf::from("/tmp").as_path(), - "-rwxr-xr-x 1 0 9 4096 Nov 5 16:32 omar.txt", - ) - .ok() - .unwrap() - .unwrap_file(); - assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt")); - assert_eq!(file.name, String::from("omar.txt")); - assert_eq!(file.size, 4096); - assert!(file.symlink.is_none()); - assert_eq!(file.user, Some(0)); - assert_eq!(file.group, Some(9)); - assert_eq!(file.unix_pex.unwrap(), (7, 5, 5)); - assert_eq!( - fmt_time(file.last_access_time, "%m %d %M").as_str(), - "11 05 32" - ); - assert_eq!( - fmt_time(file.last_change_time, "%m %d %M").as_str(), - "11 05 32" - ); - assert_eq!( - fmt_time(file.creation_time, "%m %d %M").as_str(), - "11 05 32" - ); - // Directory - let dir: FsDirectory = ftp - .parse_list_line( - PathBuf::from("/tmp").as_path(), - "drwxrwxr-x 1 0 9 4096 Nov 5 2018 docs", - ) - .ok() - .unwrap() - .unwrap_dir(); - assert_eq!(dir.abs_path, PathBuf::from("/tmp/docs")); - assert_eq!(dir.name, String::from("docs")); - assert!(dir.symlink.is_none()); - assert_eq!(dir.user, Some(0)); - assert_eq!(dir.group, Some(9)); - assert_eq!(dir.unix_pex.unwrap(), (7, 7, 5)); - assert_eq!( - dir.last_access_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1541376000) - ); - assert_eq!( - dir.last_change_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1541376000) - ); - assert_eq!( - dir.creation_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1541376000) - ); - assert_eq!(dir.readonly, false); - // Error - assert!(ftp - .parse_list_line( - PathBuf::from("/").as_path(), - "drwxrwxr-x 1 0 9 Nov 5 2018 docs" - ) - .is_err()); - } - - #[test] - fn test_filetransfer_ftp_parse_list_line_dos() { - let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); - // Simple file - let file: FsFile = ftp - .parse_list_line( - PathBuf::from("/tmp").as_path(), - "04-08-14 03:09PM 8192 omar.txt", - ) - .ok() - .unwrap() - .unwrap_file(); - assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt")); - assert_eq!(file.name, String::from("omar.txt")); - assert_eq!(file.size, 8192); - assert!(file.symlink.is_none()); - assert_eq!(file.user, None); - assert_eq!(file.group, None); - assert_eq!(file.unix_pex, None); - assert_eq!( - file.last_access_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - assert_eq!( - file.last_change_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - assert_eq!( - file.creation_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - // Directory - let dir: FsDirectory = ftp - .parse_list_line( - PathBuf::from("/tmp").as_path(), - "04-08-14 03:09PM docs", - ) - .ok() - .unwrap() - .unwrap_dir(); - assert_eq!(dir.abs_path, PathBuf::from("/tmp/docs")); - assert_eq!(dir.name, String::from("docs")); - assert!(dir.symlink.is_none()); - assert_eq!(dir.user, None); - assert_eq!(dir.group, None); - assert_eq!(dir.unix_pex, None); - assert_eq!( - dir.last_access_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - assert_eq!( - dir.last_change_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - assert_eq!( - dir.creation_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - assert_eq!(dir.readonly, false); - // Error - assert!(ftp - .parse_list_line(PathBuf::from("/").as_path(), "04-08-14 omar.txt") - .is_err()); } #[test] @@ -1265,34 +907,20 @@ mod tests { assert!(ftp.disconnect().is_ok()); } - #[test] - fn test_filetransfer_ftp_get_name_and_link() { - let client: FtpFileTransfer = FtpFileTransfer::new(false); - assert_eq!( - client.get_name_and_link("Cargo.toml"), - (String::from("Cargo.toml"), None) - ); - assert_eq!( - client.get_name_and_link("Cargo -> Cargo.toml"), - (String::from("Cargo"), Some(PathBuf::from("Cargo.toml"))) - ); - } - #[test] fn test_filetransfer_ftp_uninitialized() { let file: FsFile = FsFile { name: String::from("omar.txt"), abs_path: PathBuf::from("/omar.txt"), - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, + last_change_time: UNIX_EPOCH, + last_access_time: UNIX_EPOCH, + creation_time: UNIX_EPOCH, size: 0, ftype: Some(String::from("txt")), // File type - readonly: true, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }; let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); assert!(ftp.change_dir(Path::new("/tmp")).is_err()); diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index fc5e7f6..39d10c0 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -44,7 +44,7 @@ pub use params::FileTransferParams; /// /// This enum defines the different transfer protocol available in termscp -#[derive(PartialEq, std::fmt::Debug, std::clone::Clone, Copy)] +#[derive(PartialEq, Debug, std::clone::Clone, Copy)] pub enum FileTransferProtocol { Sftp, Scp, @@ -54,7 +54,7 @@ pub enum FileTransferProtocol { /// ## FileTransferError /// /// FileTransferError defines the possible errors available for a file transfer -#[derive(std::fmt::Debug)] +#[derive(Debug)] pub struct FileTransferError { code: FileTransferErrorType, msg: Option, @@ -84,6 +84,8 @@ pub enum FileTransferErrorType { SslError, #[error("Could not stat directory")] DirStatFailed, + #[error("Directory already exists")] + DirectoryAlreadyExists, #[error("Failed to create file")] FileCreateDenied, #[error("No such file or directory")] @@ -180,7 +182,7 @@ pub trait FileTransfer { /// ### mkdir /// /// Make directory - /// You must return error in case the directory already exists + /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError>; /// ### remove diff --git a/src/filetransfer/scp_transfer.rs b/src/filetransfer/scp_transfer.rs index dbeeca1..400b035 100644 --- a/src/filetransfer/scp_transfer.rs +++ b/src/filetransfer/scp_transfer.rs @@ -27,7 +27,7 @@ */ // Locals use super::{FileTransfer, FileTransferError, FileTransferErrorType}; -use crate::fs::{FsDirectory, FsEntry, FsFile}; +use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; use crate::system::sshkey_storage::SshKeyStorage; use crate::utils::fmt::{fmt_time, shadow_password}; use crate::utils::parser::parse_lstime; @@ -76,6 +76,21 @@ impl ScpFileTransfer { p.to_path_buf() } + /// ### absolutize + /// + /// Absolutize target path if relative. + /// This also converts backslashes to slashes if relative + fn absolutize(wrkdir: &Path, target: &Path) -> PathBuf { + match target.is_absolute() { + true => target.to_path_buf(), + false => { + let mut p: PathBuf = wrkdir.to_path_buf(); + p.push(target); + Self::resolve(p.as_path()) + } + } + } + /// ### parse_ls_output /// /// Parse a line of `ls -l` output and tokenize the output into a `FsEntry` @@ -128,7 +143,11 @@ impl ScpFileTransfer { }; // Get unix pex - let unix_pex = (pex(0..3), pex(3..6), pex(6..9)); + let unix_pex = ( + UnixPex::from(pex(0..3)), + UnixPex::from(pex(3..6)), + UnixPex::from(pex(6..9)), + ); // Parse mtime and convert to SystemTime let mtime: SystemTime = match parse_lstime( @@ -169,7 +188,7 @@ impl ScpFileTransfer { // Get symlink; PATH mustn't be equal to filename let symlink: Option> = match symlink_path { None => None, - Some(p) => match p.file_name().unwrap_or(&std::ffi::OsStr::new("")) + Some(p) => match p.file_name().unwrap_or_else(|| std::ffi::OsStr::new("")) == file_name.as_str() { // If name is equal, don't stat path; otherwise it would get stuck @@ -218,7 +237,6 @@ impl ScpFileTransfer { last_change_time: mtime, last_access_time: mtime, creation_time: mtime, - readonly: false, symlink, user: uid, group: gid, @@ -232,7 +250,6 @@ impl ScpFileTransfer { creation_time: mtime, size: filesize, ftype: extension, - readonly: false, symlink, user: uid, group: gid, @@ -339,7 +356,7 @@ impl FileTransfer for ScpFileTransfer { // Try addresses for socket_addr in socket_addresses.iter() { debug!("Trying socket address {}", socket_addr); - match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) { + match TcpStream::connect_timeout(socket_addr, Duration::from_secs(30)) { Ok(stream) => { debug!("{} succeded", socket_addr); tcp = Some(stream); @@ -504,14 +521,7 @@ impl FileTransfer for ScpFileTransfer { match self.is_connected() { true => { let p: PathBuf = self.wrkdir.clone(); - let remote_path: PathBuf = match dir.is_absolute() { - true => PathBuf::from(dir), - false => { - let mut p: PathBuf = PathBuf::from("."); - p.push(dir); - Self::resolve(p.as_path()) - } - }; + let remote_path: PathBuf = Self::absolutize(Path::new("."), dir); info!("Changing working directory to {}", remote_path.display()); // Change directory match self.perform_shell_cmd_with_path( @@ -643,13 +653,22 @@ impl FileTransfer for ScpFileTransfer { /// ### mkdir /// /// Make directory - /// You must return error in case the directory already exists + /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> { match self.is_connected() { true => { let dir: PathBuf = Self::resolve(dir); info!("Making directory {}", dir.display()); let p: PathBuf = self.wrkdir.clone(); + // If directory already exists, return Err + let mut dir_stat_path: PathBuf = dir.clone(); + dir_stat_path.push("./"); + if self.stat(dir_stat_path.as_path()).is_ok() { + error!("Directory {} already exists", dir.display()); + return Err(FileTransferError::new( + FileTransferErrorType::DirectoryAlreadyExists, + )); + } // Mkdir dir && echo 0 match self.perform_shell_cmd_with_path( p.as_path(), @@ -763,14 +782,7 @@ impl FileTransfer for ScpFileTransfer { /// /// Stat file and return FsEntry fn stat(&mut self, path: &Path) -> Result { - let path: PathBuf = match path.is_absolute() { - true => PathBuf::from(path), - false => { - let mut p: PathBuf = self.wrkdir.clone(); - p.push(path); - Self::resolve(p.as_path()) - } - }; + let path: PathBuf = Self::absolutize(self.wrkdir.as_path(), path); match self.is_connected() { true => { let p: PathBuf = self.wrkdir.clone(); @@ -846,15 +858,7 @@ impl FileTransfer for ScpFileTransfer { ) -> Result, FileTransferError> { match self.session.as_ref() { Some(session) => { - let file_name: PathBuf = match file_name.is_absolute() { - true => PathBuf::from(file_name), - false => { - let mut p: PathBuf = self.wrkdir.clone(); - p.push(file_name); - Self::resolve(p.as_path()) - } - }; - let file_name: PathBuf = Self::resolve(file_name.as_path()); + let file_name: PathBuf = Self::absolutize(self.wrkdir.as_path(), file_name); info!( "Sending file {} to {}", local.abs_path.display(), @@ -866,7 +870,11 @@ impl FileTransfer for ScpFileTransfer { // Calculate file mode let mode: i32 = match local.unix_pex { None => 0o644, - Some((u, g, o)) => ((u as i32) << 6) + ((g as i32) << 3) + (o as i32), + Some((u, g, o)) => { + ((u.as_byte() as i32) << 6) + + ((g.as_byte() as i32) << 3) + + (o.as_byte() as i32) + } }; // Calculate mtime, atime let times: (u64, u64) = { @@ -1019,6 +1027,15 @@ mod tests { assert!(client.list_dir(&Path::new("/config")).unwrap().len() >= 4); // Make directory assert!(client.mkdir(PathBuf::from("/tmp/omar").as_path()).is_ok()); + // Remake directory (should report already exists) + assert_eq!( + client + .mkdir(PathBuf::from("/tmp/omar").as_path()) + .err() + .unwrap() + .kind(), + FileTransferErrorType::DirectoryAlreadyExists + ); // Make directory (err) assert!(client .mkdir(PathBuf::from("/root/aaaaa/pommlar").as_path()) @@ -1107,11 +1124,10 @@ mod tests { creation_time: SystemTime::UNIX_EPOCH, size: 0, ftype: Some(String::from("txt")), // File type - readonly: true, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); assert!(client .rename(&dummy, PathBuf::from("/a/b/c").as_path()) @@ -1224,9 +1240,11 @@ mod tests { .unwrap_file(); assert_eq!(entry.name.as_str(), "Cargo.toml"); assert_eq!(entry.abs_path, PathBuf::from("/tmp/Cargo.toml")); - assert_eq!(entry.unix_pex.unwrap(), (6, 4, 4)); + assert_eq!( + entry.unix_pex.unwrap(), + (UnixPex::from(6), UnixPex::from(4), UnixPex::from(4)) + ); assert_eq!(entry.size, 2056); - assert_eq!(entry.readonly, false); assert_eq!(entry.ftype.unwrap().as_str(), "toml"); assert!(entry.symlink.is_none()); // File (year) @@ -1240,9 +1258,11 @@ mod tests { .unwrap_file(); assert_eq!(entry.name.as_str(), "CODE_OF_CONDUCT.md"); assert_eq!(entry.abs_path, PathBuf::from("/tmp/CODE_OF_CONDUCT.md")); - assert_eq!(entry.unix_pex.unwrap(), (6, 6, 6)); + assert_eq!( + entry.unix_pex.unwrap(), + (UnixPex::from(6), UnixPex::from(6), UnixPex::from(6)) + ); assert_eq!(entry.size, 3368); - assert_eq!(entry.readonly, false); assert_eq!(entry.ftype.unwrap().as_str(), "md"); assert!(entry.symlink.is_none()); // Directory @@ -1256,8 +1276,10 @@ mod tests { .unwrap_dir(); assert_eq!(entry.name.as_str(), "docs"); assert_eq!(entry.abs_path, PathBuf::from("/tmp/docs")); - assert_eq!(entry.unix_pex.unwrap(), (7, 5, 5)); - assert_eq!(entry.readonly, false); + assert_eq!( + entry.unix_pex.unwrap(), + (UnixPex::from(7), UnixPex::from(5), UnixPex::from(5)) + ); assert!(entry.symlink.is_none()); // Short metadata assert!(client @@ -1305,11 +1327,10 @@ mod tests { creation_time: SystemTime::UNIX_EPOCH, size: 0, ftype: Some(String::from("txt")), // File type - readonly: true, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }; let mut scp: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); assert!(scp.change_dir(Path::new("/tmp")).is_err()); diff --git a/src/filetransfer/sftp_transfer.rs b/src/filetransfer/sftp_transfer.rs index 54dcf51..ca4ea96 100644 --- a/src/filetransfer/sftp_transfer.rs +++ b/src/filetransfer/sftp_transfer.rs @@ -27,7 +27,7 @@ */ // Locals use super::{FileTransfer, FileTransferError, FileTransferErrorType}; -use crate::fs::{FsDirectory, FsEntry, FsFile}; +use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; use crate::system::sshkey_storage::SshKeyStorage; use crate::utils::fmt::{fmt_time, shadow_password}; @@ -126,11 +126,11 @@ impl SftpFileTransfer { .map(|ext| String::from(ext.to_str().unwrap_or(""))); let uid: Option = metadata.uid; let gid: Option = metadata.gid; - let pex: Option<(u8, u8, u8)> = metadata.perm.map(|x| { + let pex: Option<(UnixPex, UnixPex, UnixPex)> = metadata.perm.map(|x| { ( - ((x >> 6) & 0x7) as u8, - ((x >> 3) & 0x7) as u8, - (x & 0x7) as u8, + UnixPex::from(((x >> 6) & 0x7) as u8), + UnixPex::from(((x >> 3) & 0x7) as u8), + UnixPex::from((x & 0x7) as u8), ) }); let size: u64 = metadata.size.unwrap_or(0); @@ -178,7 +178,6 @@ impl SftpFileTransfer { last_change_time: mtime, last_access_time: atime, creation_time: SystemTime::UNIX_EPOCH, - readonly: false, symlink, user: uid, group: gid, @@ -192,7 +191,6 @@ impl SftpFileTransfer { last_change_time: mtime, last_access_time: atime, creation_time: SystemTime::UNIX_EPOCH, - readonly: false, symlink, user: uid, group: gid, @@ -282,7 +280,7 @@ impl FileTransfer for SftpFileTransfer { // Try addresses for socket_addr in socket_addresses.iter() { debug!("Trying socket address {}", socket_addr); - match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) { + match TcpStream::connect_timeout(socket_addr, Duration::from_secs(30)) { Ok(stream) => { tcp = Some(stream); break; @@ -554,11 +552,19 @@ impl FileTransfer for SftpFileTransfer { /// ### mkdir /// /// Make directory + /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> { match self.sftp.as_ref() { Some(sftp) => { // Make directory let path: PathBuf = self.get_abs_path(PathBuf::from(dir).as_path()); + // If directory already exists, return Err + if sftp.stat(path.as_path()).is_ok() { + error!("Directory {} already exists", path.display()); + return Err(FileTransferError::new( + FileTransferErrorType::DirectoryAlreadyExists, + )); + } info!("Making directory {}", path.display()); match sftp.mkdir(path.as_path(), 0o775) { Ok(_) => Ok(()), @@ -602,7 +608,7 @@ impl FileTransfer for SftpFileTransfer { // Get directory files let directory_content: Vec = self.list_dir(d.abs_path.as_path())?; for entry in directory_content.iter() { - if let Err(err) = self.remove(&entry) { + if let Err(err) = self.remove(entry) { return Err(err); } } @@ -714,7 +720,11 @@ impl FileTransfer for SftpFileTransfer { // Calculate file mode let mode: i32 = match local.unix_pex { None => 0o644, - Some((u, g, o)) => ((u as i32) << 6) + ((g as i32) << 3) + (o as i32), + Some((u, g, o)) => { + ((u.as_byte() as i32) << 6) + + ((g.as_byte() as i32) << 3) + + (o.as_byte() as i32) + } }; debug!("File mode {:?}", mode); match sftp.open_mode( @@ -839,6 +849,15 @@ mod tests { assert!(client.list_dir(&Path::new("/config")).unwrap().len() >= 4); // Make directory assert!(client.mkdir(PathBuf::from("/tmp/omar").as_path()).is_ok()); + // Remake directory (should report already exists) + assert_eq!( + client + .mkdir(PathBuf::from("/tmp/omar").as_path()) + .err() + .unwrap() + .kind(), + FileTransferErrorType::DirectoryAlreadyExists + ); // Make directory (err) assert!(client .mkdir(PathBuf::from("/root/aaaaa/pommlar").as_path()) @@ -906,11 +925,10 @@ mod tests { creation_time: SystemTime::UNIX_EPOCH, size: 0, ftype: Some(String::from("txt")), // File type - readonly: true, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); assert!(client .rename(&dummy, PathBuf::from("/a/b/c").as_path()) @@ -1055,11 +1073,10 @@ mod tests { creation_time: SystemTime::UNIX_EPOCH, size: 0, ftype: Some(String::from("txt")), // File type - readonly: true, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }; let mut sftp: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); assert!(sftp.change_dir(Path::new("/tmp")).is_err()); diff --git a/src/fs/explorer/builder.rs b/src/fs/explorer/builder.rs index 9ba2dce..4bb2185 100644 --- a/src/fs/explorer/builder.rs +++ b/src/fs/explorer/builder.rs @@ -124,7 +124,7 @@ mod tests { let explorer: FileExplorer = FileExplorerBuilder::new().build(); // Verify assert!(!explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)); - assert_eq!(explorer.file_sorting, FileSorting::ByName); // Default + assert_eq!(explorer.file_sorting, FileSorting::Name); // Default assert_eq!(explorer.group_dirs, None); assert_eq!(explorer.stack_size, 16); } @@ -132,7 +132,7 @@ mod tests { #[test] fn test_fs_explorer_builder_new_all() { let explorer: FileExplorer = FileExplorerBuilder::new() - .with_file_sorting(FileSorting::ByModifyTime) + .with_file_sorting(FileSorting::ModifyTime) .with_group_dirs(Some(GroupDirs::First)) .with_hidden_files(true) .with_stack_size(24) @@ -140,7 +140,7 @@ mod tests { .build(); // Verify assert!(explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)); - assert_eq!(explorer.file_sorting, FileSorting::ByModifyTime); // Default + assert_eq!(explorer.file_sorting, FileSorting::ModifyTime); // Default assert_eq!(explorer.group_dirs, Some(GroupDirs::First)); assert_eq!(explorer.stack_size, 24); } diff --git a/src/fs/explorer/formatter.rs b/src/fs/explorer/formatter.rs index 54ba14d..006da84 100644 --- a/src/fs/explorer/formatter.rs +++ b/src/fs/explorer/formatter.rs @@ -354,7 +354,9 @@ impl Formatter { pex.push(file_type); match fsentry.get_unix_pex() { None => pex.push_str("?????????"), - Some((owner, group, others)) => pex.push_str(fmt_pex(owner, group, others).as_str()), + Some((owner, group, others)) => pex.push_str( + format!("{}{}{}", fmt_pex(owner), fmt_pex(group), fmt_pex(others)).as_str(), + ), } // Add to cur str, prefix and the key value format!("{}{}{:10}", cur_str, prefix, pex) @@ -533,7 +535,7 @@ impl Formatter { mod tests { use super::*; - use crate::fs::{FsDirectory, FsFile}; + use crate::fs::{FsDirectory, FsFile, UnixPex}; use pretty_assertions::assert_eq; use std::path::PathBuf; @@ -552,12 +554,11 @@ mod tests { last_access_time: t_now, creation_time: t_now, size: 8192, - readonly: false, ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); let prefix: String = String::from("h"); let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None); @@ -593,12 +594,11 @@ mod tests { last_access_time: t, creation_time: t, size: 8192, - readonly: false, ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); #[cfg(target_family = "unix")] assert_eq!( @@ -624,12 +624,11 @@ mod tests { last_access_time: t, creation_time: t, size: 8192, - readonly: false, ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); #[cfg(target_family = "unix")] assert_eq!( @@ -655,7 +654,6 @@ mod tests { last_access_time: t, creation_time: t, size: 8192, - readonly: false, ftype: Some(String::from("txt")), symlink: None, // UNIX only user: Some(0), // UNIX only @@ -686,7 +684,6 @@ mod tests { last_access_time: t, creation_time: t, size: 8192, - readonly: false, ftype: Some(String::from("txt")), symlink: None, // UNIX only user: None, // UNIX only @@ -723,11 +720,10 @@ mod tests { last_change_time: t_now, last_access_time: t_now, creation_time: t_now, - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((7, 5, 5)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only }); #[cfg(target_family = "unix")] assert_eq!( @@ -752,7 +748,6 @@ mod tests { last_change_time: t_now, last_access_time: t_now, creation_time: t_now, - readonly: false, symlink: None, // UNIX only user: None, // UNIX only group: Some(0), // UNIX only @@ -789,7 +784,6 @@ mod tests { last_access_time: t, creation_time: t, size: 8192, - readonly: false, ftype: Some(String::from("txt")), symlink: None, // UNIX only user: None, // UNIX only @@ -802,11 +796,10 @@ mod tests { last_change_time: t, last_access_time: t, creation_time: t, - readonly: false, symlink: Some(Box::new(pointer)), // UNIX only user: None, // UNIX only group: None, // UNIX only - unix_pex: Some((7, 5, 5)), // UNIX only + unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only }); assert_eq!(formatter.fmt(&entry), format!( "projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}", @@ -821,11 +814,10 @@ mod tests { last_change_time: t, last_access_time: t, creation_time: t, - readonly: false, - symlink: None, // UNIX only - user: None, // UNIX only - group: None, // UNIX only - unix_pex: Some((7, 5, 5)), // UNIX only + symlink: None, // UNIX only + user: None, // UNIX only + group: None, // UNIX only + unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only }); assert_eq!(formatter.fmt(&entry), format!( "projects/ 0 0 drwxr-xr-x {} {} {}", @@ -841,7 +833,6 @@ mod tests { last_access_time: t, creation_time: t, size: 8192, - readonly: false, ftype: Some(String::from("txt")), symlink: None, // UNIX only user: None, // UNIX only @@ -855,12 +846,11 @@ mod tests { last_access_time: t, creation_time: t, size: 8192, - readonly: false, ftype: Some(String::from("txt")), symlink: Some(Box::new(pointer)), // UNIX only user: None, // UNIX only group: None, // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); assert_eq!(formatter.fmt(&entry), format!( "bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}", @@ -876,12 +866,11 @@ mod tests { last_access_time: t, creation_time: t, size: 8192, - readonly: false, ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: None, // UNIX only - group: None, // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: None, // UNIX only + group: None, // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); assert_eq!(formatter.fmt(&entry), format!( "bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}", diff --git a/src/fs/explorer/mod.rs b/src/fs/explorer/mod.rs index 3d33421..cb5436f 100644 --- a/src/fs/explorer/mod.rs +++ b/src/fs/explorer/mod.rs @@ -52,10 +52,10 @@ bitflags! { /// FileSorting defines the criteria for sorting files #[derive(Copy, Clone, PartialEq, std::fmt::Debug)] pub enum FileSorting { - ByName, - ByModifyTime, - ByCreationTime, - BySize, + Name, + ModifyTime, + CreationTime, + Size, } /// ## GroupDirs @@ -87,7 +87,7 @@ impl Default for FileExplorer { wrkdir: PathBuf::from("/"), dirstack: VecDeque::with_capacity(16), stack_size: 16, - file_sorting: FileSorting::ByName, + file_sorting: FileSorting::Name, group_dirs: None, opts: ExplorerOpts::empty(), fmt: Formatter::default(), @@ -237,10 +237,10 @@ impl FileExplorer { fn sort(&mut self) { // Choose sorting method match &self.file_sorting { - FileSorting::ByName => self.sort_files_by_name(), - FileSorting::ByCreationTime => self.sort_files_by_creation_time(), - FileSorting::ByModifyTime => self.sort_files_by_mtime(), - FileSorting::BySize => self.sort_files_by_size(), + FileSorting::Name => self.sort_files_by_name(), + FileSorting::CreationTime => self.sort_files_by_creation_time(), + FileSorting::ModifyTime => self.sort_files_by_mtime(), + FileSorting::Size => self.sort_files_by_size(), } // Directories first (NOTE: MUST COME AFTER OTHER SORTING) // Group directories if necessary @@ -318,10 +318,10 @@ impl FileExplorer { impl ToString for FileSorting { fn to_string(&self) -> String { String::from(match self { - FileSorting::ByCreationTime => "by_creation_time", - FileSorting::ByModifyTime => "by_mtime", - FileSorting::ByName => "by_name", - FileSorting::BySize => "by_size", + FileSorting::CreationTime => "by_creation_time", + FileSorting::ModifyTime => "by_mtime", + FileSorting::Name => "by_name", + FileSorting::Size => "by_size", }) } } @@ -330,10 +330,10 @@ impl FromStr for FileSorting { type Err = (); fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { - "by_creation_time" => Ok(FileSorting::ByCreationTime), - "by_mtime" => Ok(FileSorting::ByModifyTime), - "by_name" => Ok(FileSorting::ByName), - "by_size" => Ok(FileSorting::BySize), + "by_creation_time" => Ok(FileSorting::CreationTime), + "by_mtime" => Ok(FileSorting::ModifyTime), + "by_name" => Ok(FileSorting::Name), + "by_size" => Ok(FileSorting::Size), _ => Err(()), } } @@ -363,7 +363,7 @@ impl FromStr for GroupDirs { mod tests { use super::*; - use crate::fs::{FsDirectory, FsFile}; + use crate::fs::{FsDirectory, FsFile, UnixPex}; use crate::utils::fmt::fmt_time; use pretty_assertions::assert_eq; @@ -380,8 +380,8 @@ mod tests { assert_eq!(explorer.wrkdir, PathBuf::from("/")); assert_eq!(explorer.stack_size, 16); assert_eq!(explorer.group_dirs, None); - assert_eq!(explorer.file_sorting, FileSorting::ByName); - assert_eq!(explorer.get_file_sorting(), FileSorting::ByName); + assert_eq!(explorer.file_sorting, FileSorting::Name); + assert_eq!(explorer.get_file_sorting(), FileSorting::Name); } #[test] @@ -459,7 +459,7 @@ mod tests { make_fs_entry("Cargo.lock", false), make_fs_entry("codecov.yml", false), ]); - explorer.sort_by(FileSorting::ByName); + explorer.sort_by(FileSorting::Name); // First entry should be "Cargo.lock" assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock"); // Last should be "src/" @@ -475,7 +475,7 @@ mod tests { let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false); // Create files (files are then sorted by name) explorer.set_files(vec![entry1, entry2]); - explorer.sort_by(FileSorting::ByModifyTime); + explorer.sort_by(FileSorting::ModifyTime); // First entry should be "CODE_OF_CONDUCT.md" assert_eq!( explorer.files.get(0).unwrap().get_name(), @@ -494,7 +494,7 @@ mod tests { let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false); // Create files (files are then sorted by name) explorer.set_files(vec![entry1, entry2]); - explorer.sort_by(FileSorting::ByCreationTime); + explorer.sort_by(FileSorting::CreationTime); // First entry should be "CODE_OF_CONDUCT.md" assert_eq!( explorer.files.get(0).unwrap().get_name(), @@ -513,7 +513,7 @@ mod tests { make_fs_entry("src/", true), make_fs_entry_with_size("CONTRIBUTING.md", false, 256), ]); - explorer.sort_by(FileSorting::BySize); + explorer.sort_by(FileSorting::Size); // Directory has size 4096 assert_eq!(explorer.files.get(0).unwrap().get_name(), "src/"); assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); @@ -536,7 +536,7 @@ mod tests { make_fs_entry("Cargo.lock", false), make_fs_entry("codecov.yml", false), ]); - explorer.sort_by(FileSorting::ByName); + explorer.sort_by(FileSorting::Name); explorer.group_dirs_by(Some(GroupDirs::First)); // First entry should be "docs" assert_eq!(explorer.files.get(0).unwrap().get_name(), "docs/"); @@ -563,7 +563,7 @@ mod tests { make_fs_entry("Cargo.lock", false), make_fs_entry("codecov.yml", false), ]); - explorer.sort_by(FileSorting::ByName); + explorer.sort_by(FileSorting::Name); explorer.group_dirs_by(Some(GroupDirs::Last)); // Last entry should be "src" assert_eq!(explorer.files.get(8).unwrap().get_name(), "docs/"); @@ -586,12 +586,11 @@ mod tests { last_access_time: t, creation_time: t, size: 8192, - readonly: false, ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); #[cfg(target_family = "unix")] assert_eq!( @@ -614,25 +613,25 @@ mod tests { #[test] fn test_fs_explorer_to_string_from_str_traits() { // File Sorting - assert_eq!(FileSorting::ByCreationTime.to_string(), "by_creation_time"); - assert_eq!(FileSorting::ByModifyTime.to_string(), "by_mtime"); - assert_eq!(FileSorting::ByName.to_string(), "by_name"); - assert_eq!(FileSorting::BySize.to_string(), "by_size"); + assert_eq!(FileSorting::CreationTime.to_string(), "by_creation_time"); + assert_eq!(FileSorting::ModifyTime.to_string(), "by_mtime"); + assert_eq!(FileSorting::Name.to_string(), "by_name"); + assert_eq!(FileSorting::Size.to_string(), "by_size"); assert_eq!( FileSorting::from_str("by_creation_time").ok().unwrap(), - FileSorting::ByCreationTime + FileSorting::CreationTime ); assert_eq!( FileSorting::from_str("by_mtime").ok().unwrap(), - FileSorting::ByModifyTime + FileSorting::ModifyTime ); assert_eq!( FileSorting::from_str("by_name").ok().unwrap(), - FileSorting::ByName + FileSorting::Name ); assert_eq!( FileSorting::from_str("by_size").ok().unwrap(), - FileSorting::BySize + FileSorting::Size ); assert!(FileSorting::from_str("omar").is_err()); // Group dirs @@ -670,12 +669,11 @@ mod tests { last_access_time: t_now, creation_time: t_now, size: 64, - ftype: None, // File type - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + ftype: None, // File type + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }), true => FsEntry::Directory(FsDirectory { name: name.to_string(), @@ -683,11 +681,10 @@ mod tests { last_change_time: t_now, last_access_time: t_now, creation_time: t_now, - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((7, 5, 5)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only }), } } @@ -702,12 +699,11 @@ mod tests { last_access_time: t_now, creation_time: t_now, size: size, - ftype: None, // File type - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + ftype: None, // File type + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }), true => FsEntry::Directory(FsDirectory { name: name.to_string(), @@ -715,11 +711,10 @@ mod tests { last_change_time: t_now, last_access_time: t_now, creation_time: t_now, - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((7, 5, 5)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only }), } } diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 07e06a9..d04d3f5 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -52,11 +52,10 @@ pub struct FsDirectory { pub last_change_time: SystemTime, pub last_access_time: SystemTime, pub creation_time: SystemTime, - pub readonly: bool, - pub symlink: Option>, // UNIX only - pub user: Option, // UNIX only - pub group: Option, // UNIX only - pub unix_pex: Option<(u8, u8, u8)>, // UNIX only + pub symlink: Option>, // UNIX only + pub user: Option, // UNIX only + pub group: Option, // UNIX only + pub unix_pex: Option<(UnixPex, UnixPex, UnixPex)>, // UNIX only } /// ### FsFile @@ -71,12 +70,72 @@ pub struct FsFile { pub last_access_time: SystemTime, pub creation_time: SystemTime, pub size: usize, - pub ftype: Option, // File type - pub readonly: bool, - pub symlink: Option>, // UNIX only - pub user: Option, // UNIX only - pub group: Option, // UNIX only - pub unix_pex: Option<(u8, u8, u8)>, // UNIX only + pub ftype: Option, // File type + pub symlink: Option>, // UNIX only + pub user: Option, // UNIX only + pub group: Option, // UNIX only + pub unix_pex: Option<(UnixPex, UnixPex, UnixPex)>, // UNIX only +} + +/// ## UnixPex +/// +/// Describes the permissions on POSIX system. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct UnixPex { + read: bool, + write: bool, + execute: bool, +} + +impl UnixPex { + /// ### new + /// + /// Instantiates a new `UnixPex` + pub fn new(read: bool, write: bool, execute: bool) -> Self { + Self { + read, + write, + execute, + } + } + + /// ### can_read + /// + /// Returns whether user can read + pub fn can_read(&self) -> bool { + self.read + } + + /// ### can_write + /// + /// Returns whether user can write + pub fn can_write(&self) -> bool { + self.write + } + + /// ### can_execute + /// + /// Returns whether user can execute + pub fn can_execute(&self) -> bool { + self.execute + } + + /// ### as_byte + /// + /// Convert permission to byte as on POSIX systems + pub fn as_byte(&self) -> u8 { + ((self.read as u8) << 2) + ((self.write as u8) << 1) + (self.execute as u8) + } +} + +impl From for UnixPex { + fn from(bits: u8) -> Self { + Self { + read: ((bits >> 2) & 0x01) != 0, + write: ((bits >> 1) & 0x01) != 0, + execute: (bits & 0x01) != 0, + } + } } impl FsEntry { @@ -173,7 +232,7 @@ impl FsEntry { /// ### get_unix_pex /// /// Get unix pex from `FsEntry` - pub fn get_unix_pex(&self) -> Option<(u8, u8, u8)> { + pub fn get_unix_pex(&self) -> Option<(UnixPex, UnixPex, UnixPex)> { match self { FsEntry::Directory(dir) => dir.unix_pex, FsEntry::File(file) => file.unix_pex, @@ -264,11 +323,10 @@ mod tests { last_change_time: t_now, last_access_time: t_now, creation_time: t_now, - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((7, 5, 5)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only }); assert_eq!(entry.get_abs_path(), PathBuf::from("/foo")); assert_eq!(entry.get_name(), String::from("foo")); @@ -282,7 +340,10 @@ mod tests { assert_eq!(entry.is_symlink(), false); assert_eq!(entry.is_dir(), true); assert_eq!(entry.is_file(), false); - assert_eq!(entry.get_unix_pex(), Some((7, 5, 5))); + assert_eq!( + entry.get_unix_pex(), + Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))) + ); assert_eq!(entry.unwrap_dir().abs_path, PathBuf::from("/foo")); } @@ -296,12 +357,11 @@ mod tests { last_access_time: t_now, creation_time: t_now, size: 8192, - readonly: false, ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); assert_eq!(entry.get_abs_path(), PathBuf::from("/bar.txt")); assert_eq!(entry.get_name(), String::from("bar.txt")); @@ -312,7 +372,10 @@ mod tests { assert_eq!(entry.get_ftype(), Some(String::from("txt"))); assert_eq!(entry.get_user(), Some(0)); assert_eq!(entry.get_group(), Some(0)); - assert_eq!(entry.get_unix_pex(), Some((6, 4, 4))); + assert_eq!( + entry.get_unix_pex(), + Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))) + ); assert_eq!(entry.is_symlink(), false); assert_eq!(entry.is_dir(), false); assert_eq!(entry.is_file(), true); @@ -330,12 +393,11 @@ mod tests { last_access_time: t_now, creation_time: t_now, size: 8192, - readonly: false, ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); entry.unwrap_dir(); } @@ -350,11 +412,10 @@ mod tests { last_change_time: t_now, last_access_time: t_now, creation_time: t_now, - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((7, 5, 5)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only }); entry.unwrap_file(); } @@ -369,12 +430,11 @@ mod tests { last_access_time: t_now, creation_time: t_now, size: 8192, - readonly: false, ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); assert_eq!(entry.is_hidden(), false); let entry: FsEntry = FsEntry::File(FsFile { @@ -384,12 +444,11 @@ mod tests { last_access_time: t_now, creation_time: t_now, size: 8192, - readonly: false, ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); assert_eq!(entry.is_hidden(), true); let entry: FsEntry = FsEntry::Directory(FsDirectory { @@ -398,11 +457,10 @@ mod tests { last_change_time: t_now, last_access_time: t_now, creation_time: t_now, - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((7, 5, 5)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only }); assert_eq!(entry.is_hidden(), true); } @@ -418,12 +476,11 @@ mod tests { last_access_time: t_now, creation_time: t_now, size: 8192, - readonly: false, ftype: Some(String::from("txt")), - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }); // Symlink is None... assert_eq!( @@ -437,11 +494,10 @@ mod tests { last_change_time: t_now, last_access_time: t_now, creation_time: t_now, - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((7, 5, 5)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only }); assert_eq!(entry.get_realfile().get_abs_path(), PathBuf::from("/foo")); } @@ -457,11 +513,10 @@ mod tests { last_change_time: t_now, last_access_time: t_now, creation_time: t_now, - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((7, 7, 7)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))), // UNIX only }); let entry_child: FsEntry = FsEntry::Directory(FsDirectory { name: String::from("projects"), @@ -469,11 +524,10 @@ mod tests { last_change_time: t_now, last_access_time: t_now, creation_time: t_now, - readonly: false, symlink: Some(Box::new(entry_target)), user: Some(0), group: Some(0), - unix_pex: Some((7, 7, 7)), + unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))), }); let entry_root: FsEntry = FsEntry::File(FsFile { name: String::from("projects"), @@ -482,12 +536,11 @@ mod tests { last_access_time: t_now, creation_time: t_now, size: 8, - readonly: false, ftype: None, symlink: Some(Box::new(entry_child)), user: Some(0), group: Some(0), - unix_pex: Some((7, 7, 7)), + unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))), }); assert_eq!(entry_root.is_symlink(), true); // get real file @@ -498,4 +551,28 @@ mod tests { PathBuf::from("/home/cvisintin/projects") ); } + + #[test] + fn unix_pex() { + let pex: UnixPex = UnixPex::from(4); + assert_eq!(pex.can_read(), true); + assert_eq!(pex.can_write(), false); + assert_eq!(pex.can_execute(), false); + let pex: UnixPex = UnixPex::from(0); + assert_eq!(pex.can_read(), false); + assert_eq!(pex.can_write(), false); + assert_eq!(pex.can_execute(), false); + let pex: UnixPex = UnixPex::from(3); + assert_eq!(pex.can_read(), false); + assert_eq!(pex.can_write(), true); + assert_eq!(pex.can_execute(), true); + let pex: UnixPex = UnixPex::from(7); + assert_eq!(pex.can_read(), true); + assert_eq!(pex.can_write(), true); + assert_eq!(pex.can_execute(), true); + let pex: UnixPex = UnixPex::from(3); + assert_eq!(pex.as_byte(), 3); + let pex: UnixPex = UnixPex::from(7); + assert_eq!(pex.as_byte(), 7); + } } diff --git a/src/host/mod.rs b/src/host/mod.rs index f9913de..4440840 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -38,7 +38,8 @@ use std::fs::set_permissions; use std::os::unix::fs::{MetadataExt, PermissionsExt}; // Locals -use crate::fs::{FsDirectory, FsEntry, FsFile}; +use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; +use crate::utils::path; /// ## HostErrorType /// @@ -461,7 +462,6 @@ impl Localhost { last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), - readonly: attr.permissions().readonly(), symlink: match fs::read_link(path.as_path()) { Ok(p) => match self.stat(p.as_path()) { Ok(entry) => Some(Box::new(entry)), @@ -484,7 +484,6 @@ impl Localhost { last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), - readonly: attr.permissions().readonly(), size: attr.len() as usize, ftype: extension, symlink: match fs::read_link(path.as_path()) { @@ -506,7 +505,6 @@ impl Localhost { /// /// Stat file and create a FsEntry #[cfg(target_os = "windows")] - #[cfg(not(tarpaulin_include))] pub fn stat(&self, path: &Path) -> Result { let path: PathBuf = self.to_abs_path(path); info!("Stating file {}", path.display()); @@ -530,7 +528,6 @@ impl Localhost { last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), - readonly: attr.permissions().readonly(), symlink: match fs::read_link(path.as_path()) { Ok(p) => match self.stat(p.as_path()) { Ok(entry) => Some(Box::new(entry)), @@ -554,7 +551,6 @@ impl Localhost { last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), - readonly: attr.permissions().readonly(), size: attr.len() as usize, ftype: extension, symlink: match fs::read_link(path.as_path()) { @@ -789,10 +785,10 @@ impl Localhost { /// /// Return string with format xxxxxx to tuple of permissions (user, group, others) #[cfg(target_family = "unix")] - fn u32_to_mode(&self, mode: u32) -> (u8, u8, u8) { - let user: u8 = ((mode >> 6) & 0x7) as u8; - let group: u8 = ((mode >> 3) & 0x7) as u8; - let others: u8 = (mode & 0x7) as u8; + fn u32_to_mode(&self, mode: u32) -> (UnixPex, UnixPex, UnixPex) { + let user: UnixPex = UnixPex::from(((mode >> 6) & 0x7) as u8); + let group: UnixPex = UnixPex::from(((mode >> 3) & 0x7) as u8); + let others: UnixPex = UnixPex::from((mode & 0x7) as u8); (user, group, others) } @@ -808,15 +804,7 @@ impl Localhost { /// /// Convert path to absolute path fn to_abs_path(&self, p: &Path) -> PathBuf { - // Convert to abs path - match p.is_relative() { - true => { - let mut path: PathBuf = self.wrkdir.clone(); - path.push(p); - path - } - false => PathBuf::from(p), - } + path::absolutize(self.wrkdir.as_path(), p) } } diff --git a/src/lib.rs b/src/lib.rs index b6e3d0e..b1f2840 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,6 @@ extern crate content_inspector; extern crate crossterm; extern crate dirs; extern crate edit; -extern crate ftp4; extern crate hostname; #[cfg(feature = "with-keyring")] extern crate keyring; @@ -54,8 +53,10 @@ extern crate path_slash; extern crate rand; extern crate regex; extern crate ssh2; +extern crate suppaftp; extern crate tempfile; extern crate textwrap; +extern crate tui_realm_stdlib; extern crate tuirealm; extern crate ureq; #[cfg(target_family = "unix")] diff --git a/src/system/bookmarks_client.rs b/src/system/bookmarks_client.rs index ed40f65..4fc4805 100644 --- a/src/system/bookmarks_client.rs +++ b/src/system/bookmarks_client.rs @@ -115,7 +115,7 @@ impl BookmarksClient { if let Err(e) = key_storage.set_key(service_id, key.as_str()) { error!("Failed to set new key into storage: {}", e); return Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, format!("Could not write key to storage: {}", e), )); } @@ -125,7 +125,7 @@ impl BookmarksClient { _ => { error!("Failed to get key from storage: {}", e); return Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, format!("Could not get key from storage: {}", e), )); } @@ -328,7 +328,7 @@ impl BookmarksClient { Err(err) => { error!("Failed to write bookmarks: {}", err); Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, err.to_string(), )) } @@ -358,7 +358,7 @@ impl BookmarksClient { Err(err) => { error!("Failed to read bookmarks: {}", err); Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, err.to_string(), )) } @@ -407,7 +407,7 @@ impl BookmarksClient { match crypto::aes128_b64_decrypt(self.key.as_str(), secret) { Ok(txt) => Ok(txt), Err(err) => Err(SerializerError::new_ex( - SerializerErrorKind::SyntaxError, + SerializerErrorKind::Syntax, err.to_string(), )), } diff --git a/src/system/config_client.rs b/src/system/config_client.rs index 12a77d3..2a484e2 100644 --- a/src/system/config_client.rs +++ b/src/system/config_client.rs @@ -76,7 +76,7 @@ impl ConfigClient { if let Err(err) = create_dir(ssh_key_dir) { error!("Failed to create SSH key dir: {}", err); return Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, format!( "Could not create SSH key directory \"{}\": {}", ssh_key_dir.display(), @@ -252,7 +252,7 @@ impl ConfigClient { ) -> Result<(), SerializerError> { if self.degraded { return Err(SerializerError::new_ex( - SerializerErrorKind::GenericError, + SerializerErrorKind::Generic, String::from("Configuration won't be saved, since in degraded mode"), )); } @@ -291,7 +291,7 @@ impl ConfigClient { pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> { if self.degraded { return Err(SerializerError::new_ex( - SerializerErrorKind::GenericError, + SerializerErrorKind::Generic, String::from("Configuration won't be saved, since in degraded mode"), )); } @@ -351,7 +351,7 @@ impl ConfigClient { pub fn write_config(&self) -> Result<(), SerializerError> { if self.degraded { return Err(SerializerError::new_ex( - SerializerErrorKind::GenericError, + SerializerErrorKind::Generic, String::from("Configuration won't be saved, since in degraded mode"), )); } @@ -366,7 +366,7 @@ impl ConfigClient { Err(err) => { error!("Failed to write configuration file: {}", err); Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, err.to_string(), )) } @@ -379,7 +379,7 @@ impl ConfigClient { pub fn read_config(&mut self) -> Result<(), SerializerError> { if self.degraded { return Err(SerializerError::new_ex( - SerializerErrorKind::GenericError, + SerializerErrorKind::Generic, String::from("Configuration won't be loaded, since in degraded mode"), )); } @@ -401,7 +401,7 @@ impl ConfigClient { Err(err) => { error!("Failed to read configuration: {}", err); Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, err.to_string(), )) } @@ -432,7 +432,7 @@ impl ConfigClient { /// Make serializer error from `std::io::Error` fn make_io_err(err: std::io::Error) -> Result<(), SerializerError> { Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, err.to_string(), )) } diff --git a/src/system/theme_provider.rs b/src/system/theme_provider.rs index d878eb4..643687b 100644 --- a/src/system/theme_provider.rs +++ b/src/system/theme_provider.rs @@ -116,7 +116,7 @@ impl ThemeProvider { warn!("Configuration won't be loaded, since degraded; reloading default..."); self.theme = Theme::default(); return Err(SerializerError::new_ex( - SerializerErrorKind::GenericError, + SerializerErrorKind::Generic, String::from("Can't access theme file"), )); } @@ -139,7 +139,7 @@ impl ThemeProvider { Err(err) => { error!("Failed to read theme: {}", err); Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, err.to_string(), )) } @@ -153,7 +153,7 @@ impl ThemeProvider { if self.degraded { warn!("Configuration won't be saved, since in degraded mode"); return Err(SerializerError::new_ex( - SerializerErrorKind::GenericError, + SerializerErrorKind::Generic, String::from("Can't access theme file"), )); } @@ -169,7 +169,7 @@ impl ThemeProvider { Err(err) => { error!("Failed to write theme: {}", err); Err(SerializerError::new_ex( - SerializerErrorKind::IoError, + SerializerErrorKind::Io, err.to_string(), )) } diff --git a/src/ui/activities/auth/bookmarks.rs b/src/ui/activities/auth/bookmarks.rs index 44c4499..dcd9946 100644 --- a/src/ui/activities/auth/bookmarks.rs +++ b/src/ui/activities/auth/bookmarks.rs @@ -32,7 +32,7 @@ use crate::system::environment; // Ext use std::path::PathBuf; -use tuirealm::components::{input::InputPropsBuilder, radio::RadioPropsBuilder}; +use tui_realm_stdlib::{input::InputPropsBuilder, radio::RadioPropsBuilder}; use tuirealm::{Payload, PropsBuilder, Value}; impl AuthActivity { @@ -44,7 +44,7 @@ impl AuthActivity { // Iterate over kyes let name: Option<&String> = self.bookmarks_list.get(idx); if let Some(name) = name { - bookmarks_cli.del_bookmark(&name); + bookmarks_cli.del_bookmark(name); // Write bookmarks self.write_bookmarks(); } @@ -60,7 +60,7 @@ impl AuthActivity { if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() { // Iterate over bookmarks if let Some(key) = self.bookmarks_list.get(idx) { - if let Some(bookmark) = bookmarks_cli.get_bookmark(&key) { + if let Some(bookmark) = bookmarks_cli.get_bookmark(key) { // Load parameters into components self.load_bookmark_into_gui( bookmark.0, bookmark.1, bookmark.2, bookmark.3, bookmark.4, @@ -104,7 +104,7 @@ impl AuthActivity { if let Some(client) = self.bookmarks_client.as_mut() { let name: Option<&String> = self.recents_list.get(idx); if let Some(name) = name { - client.del_recent(&name); + client.del_recent(name); // Write bookmarks self.write_bookmarks(); } diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index 9c6822a..dd2275d 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -35,7 +35,7 @@ use super::{ COMPONENT_TEXT_HELP, COMPONENT_TEXT_NEW_VERSION_NOTES, COMPONENT_TEXT_SIZE_ERR, }; use crate::ui::keymap::*; -use tuirealm::components::InputPropsBuilder; +use tui_realm_stdlib::InputPropsBuilder; use tuirealm::{Msg, Payload, PropsBuilder, Update, Value}; // -- update @@ -52,53 +52,53 @@ impl Update for AuthActivity { None => None, // Exit after None Some(msg) => match msg { // Focus ( DOWN ) - (COMPONENT_RADIO_PROTOCOL, &MSG_KEY_DOWN) => { + (COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => { // Give focus to port self.view.active(COMPONENT_INPUT_ADDR); None } - (COMPONENT_INPUT_ADDR, &MSG_KEY_DOWN) => { + (COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => { // Give focus to port self.view.active(COMPONENT_INPUT_PORT); None } - (COMPONENT_INPUT_PORT, &MSG_KEY_DOWN) => { + (COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => { // Give focus to port self.view.active(COMPONENT_INPUT_USERNAME); None } - (COMPONENT_INPUT_USERNAME, &MSG_KEY_DOWN) => { + (COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => { // Give focus to port self.view.active(COMPONENT_INPUT_PASSWORD); None } - (COMPONENT_INPUT_PASSWORD, &MSG_KEY_DOWN) => { + (COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => { // Give focus to port self.view.active(COMPONENT_RADIO_PROTOCOL); None } // Focus ( UP ) - (COMPONENT_INPUT_PASSWORD, &MSG_KEY_UP) => { + (COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => { // Give focus to port self.view.active(COMPONENT_INPUT_USERNAME); None } - (COMPONENT_INPUT_USERNAME, &MSG_KEY_UP) => { + (COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => { // Give focus to port self.view.active(COMPONENT_INPUT_PORT); None } - (COMPONENT_INPUT_PORT, &MSG_KEY_UP) => { + (COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => { // Give focus to port self.view.active(COMPONENT_INPUT_ADDR); None } - (COMPONENT_INPUT_ADDR, &MSG_KEY_UP) => { + (COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => { // Give focus to port self.view.active(COMPONENT_RADIO_PROTOCOL); None } - (COMPONENT_RADIO_PROTOCOL, &MSG_KEY_UP) => { + (COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => { // Give focus to port self.view.active(COMPONENT_INPUT_PASSWORD); None @@ -118,25 +118,25 @@ impl Update for AuthActivity { } // Bookmarks commands // / - (COMPONENT_BOOKMARKS_LIST, &MSG_KEY_RIGHT) => { + (COMPONENT_BOOKMARKS_LIST, key) if key == &MSG_KEY_RIGHT => { // Give focus to recents self.view.active(COMPONENT_RECENTS_LIST); None } - (COMPONENT_RECENTS_LIST, &MSG_KEY_LEFT) => { + (COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_LEFT => { // Give focus to bookmarks self.view.active(COMPONENT_BOOKMARKS_LIST); None } // - (COMPONENT_BOOKMARKS_LIST, &MSG_KEY_DEL) - | (COMPONENT_BOOKMARKS_LIST, &MSG_KEY_CHAR_E) => { + (COMPONENT_BOOKMARKS_LIST, key) + if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E => + { // Show delete popup self.mount_bookmark_del_dialog(); None } - (COMPONENT_RECENTS_LIST, &MSG_KEY_DEL) - | (COMPONENT_RECENTS_LIST, &MSG_KEY_CHAR_E) => { + (COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E => { // Show delete popup self.mount_recent_del_dialog(); None @@ -203,67 +203,68 @@ impl Update for AuthActivity { } } // hide tab - (COMPONENT_RADIO_BOOKMARK_DEL_RECENT, &MSG_KEY_ESC) => { + (COMPONENT_RADIO_BOOKMARK_DEL_RECENT, key) if key == &MSG_KEY_ESC => { self.umount_recent_del_dialog(); None } (COMPONENT_RADIO_BOOKMARK_DEL_RECENT, _) => None, - (COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, &MSG_KEY_ESC) => { + (COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, key) if key == &MSG_KEY_ESC => { self.umount_bookmark_del_dialog(); None } (COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, _) => None, // Error message - (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) => { + (COMPONENT_TEXT_ERROR, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => { // Umount text error self.umount_error(); None } (COMPONENT_TEXT_ERROR, _) => None, - (COMPONENT_TEXT_NEW_VERSION_NOTES, &MSG_KEY_ESC) - | (COMPONENT_TEXT_NEW_VERSION_NOTES, &MSG_KEY_ENTER) => { + (COMPONENT_TEXT_NEW_VERSION_NOTES, key) + if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => + { // Umount release notes self.umount_release_notes(); None } (COMPONENT_TEXT_NEW_VERSION_NOTES, _) => None, // Help - (_, &MSG_KEY_CTRL_H) => { + (_, key) if key == &MSG_KEY_CTRL_H => { // Show help self.mount_help(); None } // Release notes - (_, &MSG_KEY_CTRL_R) => { + (_, key) if key == &MSG_KEY_CTRL_R => { // Show release notes self.mount_release_notes(); None } - (COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => { + (COMPONENT_TEXT_HELP, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => { // Hide text help self.umount_help(); None } (COMPONENT_TEXT_HELP, _) => None, // Enter setup - (_, &MSG_KEY_CTRL_C) => { + (_, key) if key == &MSG_KEY_CTRL_C => { self.exit_reason = Some(super::ExitReason::EnterSetup); None } // Save bookmark; show popup - (_, &MSG_KEY_CTRL_S) => { + (_, key) if key == &MSG_KEY_CTRL_S => { // Show popup self.mount_bookmark_save_dialog(); // Give focus to bookmark name self.view.active(COMPONENT_INPUT_BOOKMARK_NAME); None } - (COMPONENT_INPUT_BOOKMARK_NAME, &MSG_KEY_DOWN) => { + (COMPONENT_INPUT_BOOKMARK_NAME, key) if key == &MSG_KEY_DOWN => { // Give focus to pwd self.view.active(COMPONENT_RADIO_BOOKMARK_SAVE_PWD); None } - (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, &MSG_KEY_UP) => { + (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key) if key == &MSG_KEY_UP => { // Give focus to pwd self.view.active(COMPONENT_INPUT_BOOKMARK_NAME); None @@ -291,8 +292,9 @@ impl Update for AuthActivity { self.view_bookmarks() } // Hide save bookmark - (COMPONENT_INPUT_BOOKMARK_NAME, &MSG_KEY_ESC) - | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_BOOKMARK_NAME, key) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key) + if key == &MSG_KEY_ESC => + { // Umount popup self.umount_bookmark_save_dialog(); None @@ -307,45 +309,30 @@ impl Update for AuthActivity { self.umount_quit(); None } - (COMPONENT_RADIO_QUIT, &MSG_KEY_ESC) => { + (COMPONENT_RADIO_QUIT, key) if key == &MSG_KEY_ESC => { self.umount_quit(); None } // -- text size error; block everything (COMPONENT_TEXT_SIZE_ERR, _) => None, // bookmarks - (COMPONENT_BOOKMARKS_LIST, &MSG_KEY_TAB) - | (COMPONENT_RECENTS_LIST, &MSG_KEY_TAB) => { + (COMPONENT_BOOKMARKS_LIST, key) | (COMPONENT_RECENTS_LIST, key) + if key == &MSG_KEY_TAB => + { // Give focus to address self.view.active(COMPONENT_INPUT_ADDR); None } // Any , go to bookmarks - (_, &MSG_KEY_TAB) => { + (_, key) if key == &MSG_KEY_TAB => { self.view.active(COMPONENT_BOOKMARKS_LIST); None } // On submit on any unhandled (connect) - (_, Msg::OnSubmit(_)) | (_, &MSG_KEY_ENTER) => { - // Validate fields - match self.collect_host_params() { - Err(err) => { - // mount error - self.mount_error(err); - } - Ok(params) => { - self.save_recent(); - // Set file transfer params to context - self.context_mut().set_ftparams(params); - // Set exit reason - self.exit_reason = Some(super::ExitReason::Connect); - } - } - // Return None - None - } + (_, Msg::OnSubmit(_)) => self.on_unhandled_submit(), + (_, key) if key == &MSG_KEY_ENTER => self.on_unhandled_submit(), // => Quit - (_, &MSG_KEY_ESC) => { + (_, key) if key == &MSG_KEY_ESC => { self.mount_quit(); None } @@ -367,4 +354,23 @@ impl AuthActivity { } } } + + fn on_unhandled_submit(&mut self) -> Option<(String, Msg)> { + // Validate fields + match self.collect_host_params() { + Err(err) => { + // mount error + self.mount_error(err); + } + Ok(params) => { + self.save_recent(); + // Set file transfer params to context + self.context_mut().set_ftparams(params); + // Set exit reason + self.exit_reason = Some(super::ExitReason::Connect); + } + } + // Return None + None + } } diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 8ec4763..ba3adc8 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -27,17 +27,15 @@ */ // Locals use super::{AuthActivity, Context, FileTransferProtocol}; -use crate::ui::components::{ - bookmark_list::{BookmarkList, BookmarkListPropsBuilder}, - msgbox::{MsgBox, MsgBoxPropsBuilder}, -}; +use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder}; use crate::utils::ui::draw_area_in; // Ext -use tuirealm::components::{ +use tui_realm_stdlib::{ input::{Input, InputPropsBuilder}, label::{Label, LabelPropsBuilder}, + list::{List, ListPropsBuilder}, + paragraph::{Paragraph, ParagraphPropsBuilder}, radio::{Radio, RadioPropsBuilder}, - scrolltable::{ScrollTablePropsBuilder, Scrolltable}, span::{Span, SpanPropsBuilder}, textarea::{Textarea, TextareaPropsBuilder}, }; @@ -47,7 +45,7 @@ use tuirealm::tui::{ widgets::{BorderType, Borders, Clear}, }; use tuirealm::{ - props::{InputType, PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder}, + props::{Alignment, InputType, PropsBuilder, TableBuilder, TextSpan}, Msg, Payload, Value, }; @@ -91,19 +89,11 @@ impl AuthActivity { Box::new(Span::new( SpanPropsBuilder::default() .with_spans(vec![ - TextSpanBuilder::new("Press ").bold().build(), - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - TextSpanBuilder::new(" to show keybindings; ") - .bold() - .build(), - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - TextSpanBuilder::new(" to enter setup").bold().build(), + TextSpan::new("Press ").bold(), + TextSpan::new("").bold().fg(key_color), + TextSpan::new(" to show keybindings; ").bold(), + TextSpan::new("").bold().fg(key_color), + TextSpan::new(" to enter setup").bold(), ]) .build(), )), @@ -118,16 +108,10 @@ impl AuthActivity { .with_color(protocol_color) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, protocol_color) - .with_options( - Some(String::from("Protocol")), - vec![ - String::from("SFTP"), - String::from("SCP"), - String::from("FTP"), - String::from("FTPS"), - ], - ) + .with_title("Protocol", Alignment::Left) + .with_options(&["SFTP", "SCP", "FTP", "FTPS"]) .with_value(Self::protocol_enum_to_opt(default_protocol)) + .rewind(true) .build(), )), ); @@ -138,7 +122,7 @@ impl AuthActivity { InputPropsBuilder::default() .with_foreground(addr_color) .with_borders(Borders::ALL, BorderType::Rounded, addr_color) - .with_label(String::from("Remote host")) + .with_label("Remote host", Alignment::Left) .build(), )), ); @@ -149,7 +133,7 @@ impl AuthActivity { InputPropsBuilder::default() .with_foreground(port_color) .with_borders(Borders::ALL, BorderType::Rounded, port_color) - .with_label(String::from("Port number")) + .with_label("Port number", Alignment::Left) .with_input(InputType::Number) .with_input_len(5) .with_value(Self::get_default_port_for_protocol(default_protocol).to_string()) @@ -163,7 +147,7 @@ impl AuthActivity { InputPropsBuilder::default() .with_foreground(username_color) .with_borders(Borders::ALL, BorderType::Rounded, username_color) - .with_label(String::from("Username")) + .with_label("Username", Alignment::Left) .build(), )), ); @@ -174,7 +158,7 @@ impl AuthActivity { InputPropsBuilder::default() .with_foreground(password_color) .with_borders(Borders::ALL, BorderType::Rounded, password_color) - .with_label(String::from("Password")) + .with_label("Password", Alignment::Left) .with_input(InputType::Password) .build(), )), @@ -193,7 +177,7 @@ impl AuthActivity { .with_foreground(Color::Yellow) .with_spans(vec![ TextSpan::from("termscp "), - TextSpanBuilder::new(version.as_str()).underlined().bold().build(), + TextSpan::new(version.as_str()).underlined().bold(), TextSpan::from(" is NOW available! Get it from ; view release notes with "), ]) .build(), @@ -208,7 +192,7 @@ impl AuthActivity { .with_background(bookmarks_color) .with_foreground(Color::Black) .with_borders(Borders::ALL, BorderType::Plain, bookmarks_color) - .with_bookmarks(Some(String::from("Bookmarks")), vec![]) + .with_title("Bookmarks", Alignment::Left) .build(), )), ); @@ -220,7 +204,7 @@ impl AuthActivity { .with_background(recents_color) .with_foreground(Color::Black) .with_borders(Borders::ALL, BorderType::Plain, recents_color) - .with_bookmarks(Some(String::from("Recent connections")), vec![]) + .with_title("Recent connections", Alignment::Left) .build(), )), ); @@ -426,7 +410,7 @@ impl AuthActivity { let msg = self.view.update( super::COMPONENT_BOOKMARKS_LIST, BookmarkListPropsBuilder::from(props) - .with_bookmarks(Some(String::from("Bookmarks")), bookmarks) + .with_bookmarks(bookmarks) .build(), ); msg @@ -464,7 +448,7 @@ impl AuthActivity { let msg = self.view.update( super::COMPONENT_RECENTS_LIST, BookmarkListPropsBuilder::from(props) - .with_bookmarks(Some(String::from("Recent connections")), bookmarks) + .with_bookmarks(bookmarks) .build(), ); msg @@ -482,12 +466,13 @@ impl AuthActivity { let err_color = self.theme().misc_error_dialog; self.view.mount( super::COMPONENT_TEXT_ERROR, - Box::new(MsgBox::new( - MsgBoxPropsBuilder::default() + Box::new(Paragraph::new( + ParagraphPropsBuilder::default() .with_foreground(err_color) .with_borders(Borders::ALL, BorderType::Thick, err_color) .bold() - .with_texts(None, vec![TextSpan::from(text)]) + .with_text_alignment(Alignment::Center) + .with_texts(vec![TextSpan::from(text)]) .build(), )), ); @@ -510,17 +495,15 @@ impl AuthActivity { let err_color = self.theme().misc_error_dialog; self.view.mount( super::COMPONENT_TEXT_SIZE_ERR, - Box::new(MsgBox::new( - MsgBoxPropsBuilder::default() + Box::new(Paragraph::new( + ParagraphPropsBuilder::default() .with_foreground(err_color) .with_borders(Borders::ALL, BorderType::Thick, err_color) .bold() - .with_texts( - None, - vec![TextSpan::from( - "termscp requires at least 24 lines of height to run", - )], - ) + .with_texts(vec![TextSpan::from( + "termscp requires at least 24 lines of height to run", + )]) + .with_text_alignment(Alignment::Center) .build(), )), ); @@ -548,10 +531,9 @@ impl AuthActivity { .with_color(quit_color) .with_borders(Borders::ALL, BorderType::Rounded, quit_color) .with_inverted_color(Color::Black) - .with_options( - Some(String::from("Quit termscp?")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Quit termscp?", Alignment::Center) + .with_options(&[String::from("Yes"), String::from("No")]) + .rewind(true) .build(), )), ); @@ -577,11 +559,10 @@ impl AuthActivity { .with_color(warn_color) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, warn_color) - .with_options( - Some(String::from("Delete bookmark?")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Delete bookmark?", Alignment::Center) + .with_options(&[String::from("Yes"), String::from("No")]) .with_value(1) + .rewind(true) .build(), )), ); @@ -610,11 +591,10 @@ impl AuthActivity { .with_color(warn_color) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, warn_color) - .with_options( - Some(String::from("Delete bookmark?")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Delete bookmark?", Alignment::Center) + .with_options(&[String::from("Yes"), String::from("No")]) .with_value(1) + .rewind(true) .build(), )), ); @@ -640,7 +620,7 @@ impl AuthActivity { Box::new(Input::new( InputPropsBuilder::default() .with_foreground(save_color) - .with_label(String::from("Save bookmark as…")) + .with_label("Save bookmark as…", Alignment::Center) .with_borders( Borders::TOP | Borders::RIGHT | Borders::LEFT, BorderType::Rounded, @@ -659,10 +639,9 @@ impl AuthActivity { BorderType::Rounded, Color::Reset, ) - .with_options( - Some(String::from("Save password?")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Save password?", Alignment::Center) + .with_options(&[String::from("Yes"), String::from("No")]) + .rewind(true) .build(), )), ); @@ -685,77 +664,38 @@ impl AuthActivity { let key_color = self.theme().misc_keys; self.view.mount( super::COMPONENT_TEXT_HELP, - Box::new(Scrolltable::new( - ScrollTablePropsBuilder::default() + Box::new(List::new( + ListPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, Color::White) .with_highlighted_str(Some("?")) .with_max_scroll_step(8) + .scrollable(true) .bold() - .with_table( - Some(String::from("Help")), + .with_title("Help", Alignment::Center) + .with_rows( TableBuilder::default() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Quit termscp")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Switch from form and bookmarks")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Switch bookmark tab")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Move up/down in current tab")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Connect/Load bookmark")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Delete selected bookmark")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Enter setup")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Save bookmark")) .build(), ) @@ -786,7 +726,8 @@ impl AuthActivity { Box::new(Textarea::new( TextareaPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow) - .with_texts(Some(String::from("Release notes")), spans) + .with_title("Release notes", Alignment::Center) + .with_texts(spans) .build(), )), ); diff --git a/src/ui/activities/filetransfer/actions/copy.rs b/src/ui/activities/filetransfer/actions/copy.rs index 1d28fe1..3970603 100644 --- a/src/ui/activities/filetransfer/actions/copy.rs +++ b/src/ui/activities/filetransfer/actions/copy.rs @@ -144,6 +144,8 @@ impl FileTransferActivity { /// /// Tricky copy will be used whenever copy command is not available on remote host fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) { + // NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen + self.umount_wait(); // match entry match entry { FsEntry::File(entry) => { diff --git a/src/ui/activities/filetransfer/actions/delete.rs b/src/ui/activities/filetransfer/actions/delete.rs index 3d95a19..4c1bba5 100644 --- a/src/ui/activities/filetransfer/actions/delete.rs +++ b/src/ui/activities/filetransfer/actions/delete.rs @@ -72,7 +72,7 @@ impl FileTransferActivity { } pub(crate) fn local_remove_file(&mut self, entry: &FsEntry) { - match self.host.remove(&entry) { + match self.host.remove(entry) { Ok(_) => { // Log self.log( @@ -94,7 +94,7 @@ impl FileTransferActivity { } pub(crate) fn remote_remove_file(&mut self, entry: &FsEntry) { - match self.client.remove(&entry) { + match self.client.remove(entry) { Ok(_) => { self.log( LogLevel::Info, diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index df49198..501ab92 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -142,7 +142,7 @@ impl Browser { let mut builder: FileExplorerBuilder = FileExplorerBuilder::new(); // Set common keys builder - .with_file_sorting(FileSorting::ByName) + .with_file_sorting(FileSorting::Name) .with_stack_size(16) .with_group_dirs(cli.get_group_dirs()) .with_hidden_files(cli.get_show_hidden_files()); @@ -154,7 +154,7 @@ impl Browser { /// Build explorer reading from `ConfigClient`, for found result (has some differences) fn build_found_explorer() -> FileExplorer { FileExplorerBuilder::new() - .with_file_sorting(FileSorting::ByName) + .with_file_sorting(FileSorting::Name) .with_group_dirs(Some(GroupDirs::First)) .with_hidden_files(true) .with_stack_size(0) diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index 0d1c4ac..60360d7 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -25,6 +25,7 @@ use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord}; use crate::system::environment; use crate::system::sshkey_storage::SshKeyStorage; +use crate::utils::path; // Ext use std::env; use std::path::{Path, PathBuf}; @@ -124,27 +125,13 @@ impl FileTransferActivity { /// /// Convert a path to absolute according to local explorer pub(super) fn local_to_abs_path(&self, path: &Path) -> PathBuf { - match path.is_relative() { - true => { - let mut d: PathBuf = self.local().wrkdir.clone(); - d.push(path); - d - } - false => path.to_path_buf(), - } + path::absolutize(self.local().wrkdir.as_path(), path) } /// ### remote_to_abs_path /// /// Convert a path to absolute according to remote explorer pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf { - match path.is_relative() { - true => { - let mut wrkdir: PathBuf = self.remote().wrkdir.clone(); - wrkdir.push(path); - wrkdir - } - false => path.to_path_buf(), - } + path::absolutize(self.remote().wrkdir.as_path(), path) } } diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 91da013..3d3a655 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -228,7 +228,7 @@ impl FileTransferActivity { /// /// Returns config client reference fn config(&self) -> &ConfigClient { - &self.context().config() + self.context().config() } /// ### theme diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 9ea0b12..a9a4eb7 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -27,7 +27,7 @@ */ // Locals use super::{FileTransferActivity, LogLevel}; -use crate::filetransfer::FileTransferError; +use crate::filetransfer::{FileTransferError, FileTransferErrorType}; use crate::fs::{FsEntry, FsFile}; use crate::host::HostError; use crate::utils::fmt::fmt_millis; @@ -363,41 +363,22 @@ impl FileTransferActivity { } } FsEntry::Directory(dir) => { - // Create directory on remote + // Create directory on remote first match self.client.mkdir(remote_path.as_path()) { Ok(_) => { self.log( LogLevel::Info, format!("Created directory \"{}\"", remote_path.display()), ); - // Get files in dir - match self.host.scan_dir(dir.abs_path.as_path()) { - Ok(entries) => { - // Iterate over files - for entry in entries.iter() { - // If aborted; break - if self.transfer.aborted() { - break; - } - // Send entry; name is always None after first call - self.filetransfer_send_recurse( - &entry, - remote_path.as_path(), - None, - ); - } - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Could not scan directory \"{}\": {}", - dir.abs_path.display(), - err - ), - ); - } - } + } + Err(err) if err.kind() == FileTransferErrorType::DirectoryAlreadyExists => { + self.log( + LogLevel::Info, + format!( + "Directory \"{}\" already exists on remote", + remote_path.display() + ), + ); } Err(err) => { self.log_and_alert( @@ -408,6 +389,31 @@ impl FileTransferActivity { err ), ); + return; + } + } + // Get files in dir + match self.host.scan_dir(dir.abs_path.as_path()) { + Ok(entries) => { + // Iterate over files + for entry in entries.iter() { + // If aborted; break + if self.transfer.aborted() { + break; + } + // Send entry; name is always None after first call + self.filetransfer_send_recurse(entry, remote_path.as_path(), None); + } + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!( + "Could not scan directory \"{}\": {}", + dir.abs_path.display(), + err + ), + ); } } } @@ -701,13 +707,16 @@ impl FileTransferActivity { target_os = "macos", target_os = "linux" ))] - if let Some(pex) = dir.unix_pex { - if let Err(err) = self.host.chmod(local_dir_path.as_path(), pex) { + if let Some((owner, group, others)) = dir.unix_pex { + if let Err(err) = self.host.chmod( + local_dir_path.as_path(), + (owner.as_byte(), group.as_byte(), others.as_byte()), + ) { self.log( LogLevel::Error, format!( "Could not apply file mode {:?} to \"{}\": {}", - pex, + (owner.as_byte(), group.as_byte(), others.as_byte()), local_dir_path.display(), err ), @@ -730,7 +739,7 @@ impl FileTransferActivity { // Receive entry; name is always None after first call // Local path becomes local_dir_path self.filetransfer_recv_recurse( - &entry, + entry, local_dir_path.as_path(), None, ); @@ -868,13 +877,16 @@ impl FileTransferActivity { target_os = "macos", target_os = "linux" ))] - if let Some(pex) = remote.unix_pex { - if let Err(err) = self.host.chmod(local, pex) { + if let Some((owner, group, others)) = remote.unix_pex { + if let Err(err) = self + .host + .chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte())) + { self.log( LogLevel::Error, format!( "Could not apply file mode {:?} to \"{}\": {}", - pex, + (owner.as_byte(), group.as_byte(), others.as_byte()), local.display(), err ), diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 723a0ff..3dc40fc 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -42,9 +42,9 @@ use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxProps use crate::ui::keymap::*; use crate::utils::fmt::fmt_path_elide_ex; // externals +use tui_realm_stdlib::progress_bar::ProgressBarPropsBuilder; use tuirealm::{ - components::progress_bar::ProgressBarPropsBuilder, - props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder}, + props::{Alignment, PropsBuilder, TableBuilder, TextSpan}, tui::style::Color, Msg, Payload, Update, Value, }; @@ -63,13 +63,13 @@ impl Update for FileTransferActivity { None => None, // Exit after None Some(msg) => match msg { // -- local tab - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_RIGHT) => { + (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_RIGHT => { // Change tab self.view.active(COMPONENT_EXPLORER_REMOTE); self.browser.change_tab(FileExplorerTab::Remote); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_BACKSPACE) => { + (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_BACKSPACE => { // Go to previous directory self.action_go_to_previous_local_dir(false); if self.browser.sync_browsing { @@ -98,11 +98,11 @@ impl Update for FileTransferActivity { None } } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_SPACE) => { + (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_SPACE => { self.action_local_send(); self.update_remote_filelist() } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_A) => { + (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_A => { // Toggle hidden files self.local_mut().toggle_hidden_files(); // Update status bar @@ -110,24 +110,24 @@ impl Update for FileTransferActivity { // Reload file list component self.update_local_filelist() } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_I) => { + (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_I => { if let SelectedEntry::One(file) = self.get_local_selected_entries() { self.mount_file_info(&file); } None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_L) => { + (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_L => { // Reload directory self.reload_local_dir(); // Reload file list component self.update_local_filelist() } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_O) => { + (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_O => { self.action_edit_local_file(); // Reload file list component self.update_local_filelist() } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_U) => { + (COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_U => { self.action_go_to_local_upper_dir(false); if self.browser.sync_browsing { let _ = self.update_remote_filelist(); @@ -136,7 +136,7 @@ impl Update for FileTransferActivity { self.update_local_filelist() } // -- remote tab - (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_LEFT) => { + (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_LEFT => { // Change tab self.view.active(COMPONENT_EXPLORER_LOCAL); self.browser.change_tab(FileExplorerTab::Local); @@ -162,11 +162,11 @@ impl Update for FileTransferActivity { None } } - (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_SPACE) => { + (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_SPACE => { self.action_remote_recv(); self.update_local_filelist() } - (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_BACKSPACE) => { + (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_BACKSPACE => { // Go to previous directory self.action_go_to_previous_remote_dir(false); // If sync is enabled update local too @@ -176,7 +176,7 @@ impl Update for FileTransferActivity { // Reload file list component self.update_remote_filelist() } - (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_A) => { + (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_A => { // Toggle hidden files self.remote_mut().toggle_hidden_files(); // Update status bar @@ -184,25 +184,25 @@ impl Update for FileTransferActivity { // Reload file list component self.update_remote_filelist() } - (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_I) => { + (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_I => { if let SelectedEntry::One(file) = self.get_remote_selected_entries() { self.mount_file_info(&file); } None } - (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_L) => { + (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_L => { // Reload directory self.reload_remote_dir(); // Reload file list component self.update_remote_filelist() } - (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_O) => { + (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_O => { // Edit file self.action_edit_remote_file(); // Reload file list component self.update_remote_filelist() } - (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_U) => { + (COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_U => { self.action_go_to_remote_upper_dir(false); if self.browser.sync_browsing { let _ = self.update_local_filelist(); @@ -211,64 +211,78 @@ impl Update for FileTransferActivity { self.update_remote_filelist() } // -- common explorer keys - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_B) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_B) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_CHAR_B => + { // Show sorting file self.mount_file_sorting(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_C) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_C) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_CHAR_C => + { self.mount_copy(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_D) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_D) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_CHAR_D => + { self.mount_mkdir(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_F) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_F) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_CHAR_F => + { self.mount_find_input(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_G) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_G) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_CHAR_G => + { self.mount_goto(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_H) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_H) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_CHAR_H => + { self.mount_help(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_N) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_N) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_CHAR_N => + { self.mount_newfile(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_Q) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_Q) - | (COMPONENT_LOG_BOX, &MSG_KEY_CHAR_Q) => { + (COMPONENT_EXPLORER_LOCAL, key) + | (COMPONENT_EXPLORER_REMOTE, key) + | (COMPONENT_LOG_BOX, key) + if key == &MSG_KEY_CHAR_Q => + { self.mount_quit(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_R) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_R) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_CHAR_R => + { // Mount rename self.mount_rename(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_S) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_S) - | (COMPONENT_EXPLORER_FIND, &MSG_KEY_CHAR_S) => { + (COMPONENT_EXPLORER_LOCAL, key) + | (COMPONENT_EXPLORER_REMOTE, key) + | (COMPONENT_EXPLORER_FIND, key) + if key == &MSG_KEY_CHAR_S => + { // Mount save as self.mount_saveas(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_V) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_V) - | (COMPONENT_EXPLORER_FIND, &MSG_KEY_CHAR_V) => { + (COMPONENT_EXPLORER_LOCAL, key) + | (COMPONENT_EXPLORER_REMOTE, key) + | (COMPONENT_EXPLORER_FIND, key) + if key == &MSG_KEY_CHAR_V => + { // View match self.browser.tab() { FileExplorerTab::Local => self.action_open_local(), @@ -279,44 +293,49 @@ impl Update for FileTransferActivity { } None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_W) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_W) - | (COMPONENT_EXPLORER_FIND, &MSG_KEY_CHAR_W) => { + (COMPONENT_EXPLORER_LOCAL, key) + | (COMPONENT_EXPLORER_REMOTE, key) + | (COMPONENT_EXPLORER_FIND, key) + if key == &MSG_KEY_CHAR_W => + { // Open with self.mount_openwith(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_X) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_X) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_CHAR_X => + { // Mount exec self.mount_exec(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_Y) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_Y) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_CHAR_Y => + { // Toggle browser sync self.browser.toggle_sync_browsing(); // Update status bar self.refresh_remote_status_bar(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_ESC) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_ESC) - | (COMPONENT_LOG_BOX, &MSG_KEY_ESC) => { + (COMPONENT_EXPLORER_LOCAL, key) + | (COMPONENT_EXPLORER_REMOTE, key) + | (COMPONENT_LOG_BOX, key) + if key == &MSG_KEY_ESC => + { self.mount_disconnect(); None } - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_DEL) - | (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_E) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_DEL) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_E) - | (COMPONENT_EXPLORER_FIND, &MSG_KEY_DEL) - | (COMPONENT_EXPLORER_FIND, &MSG_KEY_CHAR_E) => { + (COMPONENT_EXPLORER_LOCAL, key) + | (COMPONENT_EXPLORER_REMOTE, key) + | (COMPONENT_EXPLORER_FIND, key) + if key == &MSG_KEY_CHAR_E || key == &MSG_KEY_DEL => + { self.mount_radio_delete(); None } // -- find result explorer - (COMPONENT_EXPLORER_FIND, &MSG_KEY_ESC) => { + (COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_ESC => { // Umount find self.umount_find(); // Finalize find @@ -337,7 +356,7 @@ impl Update for FileTransferActivity { _ => None, } } - (COMPONENT_EXPLORER_FIND, &MSG_KEY_SPACE) => { + (COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_SPACE => { // Get entry self.action_find_transfer(None); // Reload files @@ -349,18 +368,19 @@ impl Update for FileTransferActivity { } } // -- switch to log - (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_TAB) - | (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_TAB) => { + (COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key) + if key == &MSG_KEY_TAB => + { self.view.active(COMPONENT_LOG_BOX); // Active log box None } // -- Log box - (COMPONENT_LOG_BOX, &MSG_KEY_TAB) => { + (COMPONENT_LOG_BOX, key) if key == &MSG_KEY_TAB => { self.view.blur(); // Blur log box None } // -- copy popup - (COMPONENT_INPUT_COPY, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_COPY, key) if key == &MSG_KEY_ESC => { self.umount_copy(); None } @@ -383,7 +403,7 @@ impl Update for FileTransferActivity { } (COMPONENT_INPUT_COPY, _) => None, // -- exec popup - (COMPONENT_INPUT_EXEC, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_EXEC, key) if key == &MSG_KEY_ESC => { self.umount_exec(); None } @@ -406,7 +426,7 @@ impl Update for FileTransferActivity { } (COMPONENT_INPUT_EXEC, _) => None, // -- find popup - (COMPONENT_INPUT_FIND, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_FIND, key) if key == &MSG_KEY_ESC => { self.umount_find_input(); None } @@ -441,7 +461,7 @@ impl Update for FileTransferActivity { None } // -- goto popup - (COMPONENT_INPUT_GOTO, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_GOTO, key) if key == &MSG_KEY_ESC => { self.umount_goto(); None } @@ -474,7 +494,7 @@ impl Update for FileTransferActivity { } (COMPONENT_INPUT_GOTO, _) => None, // -- make directory - (COMPONENT_INPUT_MKDIR, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_MKDIR, key) if key == &MSG_KEY_ESC => { self.umount_mkdir(); None } @@ -494,7 +514,7 @@ impl Update for FileTransferActivity { } (COMPONENT_INPUT_MKDIR, _) => None, // -- new file - (COMPONENT_INPUT_NEWFILE, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_NEWFILE, key) if key == &MSG_KEY_ESC => { self.umount_newfile(); None } @@ -514,7 +534,7 @@ impl Update for FileTransferActivity { } (COMPONENT_INPUT_NEWFILE, _) => None, // -- open with - (COMPONENT_INPUT_OPEN_WITH, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_OPEN_WITH, key) if key == &MSG_KEY_ESC => { self.umount_openwith(); None } @@ -531,7 +551,7 @@ impl Update for FileTransferActivity { } (COMPONENT_INPUT_OPEN_WITH, _) => None, // -- rename - (COMPONENT_INPUT_RENAME, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_RENAME, key) if key == &MSG_KEY_ESC => { self.umount_rename(); None } @@ -553,7 +573,7 @@ impl Update for FileTransferActivity { } (COMPONENT_INPUT_RENAME, _) => None, // -- save as - (COMPONENT_INPUT_SAVEAS, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_SAVEAS, key) if key == &MSG_KEY_ESC => { self.umount_saveas(); None } @@ -578,15 +598,18 @@ impl Update for FileTransferActivity { } (COMPONENT_INPUT_SAVEAS, _) => None, // -- fileinfo - (COMPONENT_LIST_FILEINFO, &MSG_KEY_ENTER) - | (COMPONENT_LIST_FILEINFO, &MSG_KEY_ESC) => { + (COMPONENT_LIST_FILEINFO, key) | (COMPONENT_LIST_FILEINFO, key) + if key == &MSG_KEY_ENTER || key == &MSG_KEY_ESC => + { self.umount_file_info(); None } (COMPONENT_LIST_FILEINFO, _) => None, // -- delete - (COMPONENT_RADIO_DELETE, &MSG_KEY_ESC) - | (COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => { + (COMPONENT_RADIO_DELETE, key) + if key == &MSG_KEY_ESC + || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => + { self.umount_radio_delete(); None } @@ -631,8 +654,10 @@ impl Update for FileTransferActivity { } (COMPONENT_RADIO_DELETE, _) => None, // -- disconnect - (COMPONENT_RADIO_DISCONNECT, &MSG_KEY_ESC) - | (COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => { + (COMPONENT_RADIO_DISCONNECT, key) + if key == &MSG_KEY_ESC + || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => + { self.umount_disconnect(); None } @@ -643,8 +668,10 @@ impl Update for FileTransferActivity { } (COMPONENT_RADIO_DISCONNECT, _) => None, // -- quit - (COMPONENT_RADIO_QUIT, &MSG_KEY_ESC) - | (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => { + (COMPONENT_RADIO_QUIT, key) + if key == &MSG_KEY_ESC + || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => + { self.umount_quit(); None } @@ -655,18 +682,21 @@ impl Update for FileTransferActivity { } (COMPONENT_RADIO_QUIT, _) => None, // -- sorting - (COMPONENT_RADIO_SORTING, &MSG_KEY_ESC) - | (COMPONENT_RADIO_SORTING, Msg::OnSubmit(_)) => { + (COMPONENT_RADIO_SORTING, key) if key == &MSG_KEY_ESC => { + self.umount_file_sorting(); + None + } + (COMPONENT_RADIO_SORTING, Msg::OnSubmit(_)) => { self.umount_file_sorting(); None } (COMPONENT_RADIO_SORTING, Msg::OnChange(Payload::One(Value::Usize(mode)))) => { // Get sorting mode let sorting: FileSorting = match mode { - 1 => FileSorting::ByModifyTime, - 2 => FileSorting::ByCreationTime, - 3 => FileSorting::BySize, - _ => FileSorting::ByName, + 1 => FileSorting::ModifyTime, + 2 => FileSorting::CreationTime, + 3 => FileSorting::Size, + _ => FileSorting::Name, }; match self.browser.tab() { FileExplorerTab::Local => self.local_mut().sort_by(sorting), @@ -688,25 +718,31 @@ impl Update for FileTransferActivity { } (COMPONENT_RADIO_SORTING, _) => None, // -- error - (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) => { + (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) + if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => + { self.umount_error(); None } (COMPONENT_TEXT_ERROR, _) => None, // -- fatal - (COMPONENT_TEXT_FATAL, &MSG_KEY_ESC) | (COMPONENT_TEXT_FATAL, &MSG_KEY_ENTER) => { + (COMPONENT_TEXT_FATAL, key) | (COMPONENT_TEXT_FATAL, key) + if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => + { self.exit_reason = Some(super::ExitReason::Disconnect); None } (COMPONENT_TEXT_FATAL, _) => None, // -- help - (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) | (COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) => { + (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) + if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => + { self.umount_help(); None } (COMPONENT_TEXT_HELP, _) => None, // -- progress bar - (COMPONENT_PROGRESS_BAR_PARTIAL, &MSG_KEY_CTRL_C) => { + (COMPONENT_PROGRESS_BAR_PARTIAL, key) if key == &MSG_KEY_CTRL_C => { // Set transfer aborted to True self.transfer.abort(); None @@ -752,7 +788,8 @@ impl FileTransferActivity { .collect(); // Update let props = FileListPropsBuilder::from(props) - .with_files(Some(hostname), files) + .with_files(files) + .with_title(hostname, Alignment::Left) .build(); // Update self.view.update(super::COMPONENT_EXPLORER_LOCAL, props) @@ -790,7 +827,8 @@ impl FileTransferActivity { .collect(); // Update let props = FileListPropsBuilder::from(props) - .with_files(Some(hostname), files) + .with_files(files) + .with_title(hostname, Alignment::Left) .build(); self.view.update(super::COMPONENT_EXPLORER_REMOTE, props) } @@ -823,7 +861,7 @@ impl FileTransferActivity { ))) .add_col(TextSpan::from(" [")) .add_col( - TextSpanBuilder::new( + TextSpan::new( format!( "{:5}", match record.level { @@ -834,16 +872,13 @@ impl FileTransferActivity { ) .as_str(), ) - .with_foreground(fg) - .build(), + .fg(fg), ) .add_col(TextSpan::from("]: ")) .add_col(TextSpan::from(record.msg.as_ref())); } let table = table.build(); - let props = LogboxPropsBuilder::from(props) - .with_log(Some(String::from("Log")), table) - .build(); + let props = LogboxPropsBuilder::from(props).with_log(table).build(); self.view.update(super::COMPONENT_LOG_BOX, props) } None => None, @@ -852,9 +887,8 @@ impl FileTransferActivity { pub(super) fn update_progress_bar(&mut self, filename: String) -> Option<(String, Msg)> { if let Some(props) = self.view.get_props(COMPONENT_PROGRESS_BAR_FULL) { - let root_name: String = props.texts.title.as_deref().unwrap_or("").to_string(); let props = ProgressBarPropsBuilder::from(props) - .with_texts(Some(root_name), self.transfer.full.to_string()) + .with_label(self.transfer.full.to_string()) .with_progress(self.transfer.full.calc_progress()) .build(); let _ = self.view.update(COMPONENT_PROGRESS_BAR_FULL, props); @@ -862,7 +896,8 @@ impl FileTransferActivity { match self.view.get_props(COMPONENT_PROGRESS_BAR_PARTIAL) { Some(props) => { let props = ProgressBarPropsBuilder::from(props) - .with_texts(Some(filename), self.transfer.partial.to_string()) + .with_title(filename, Alignment::Center) + .with_label(self.transfer.partial.to_string()) .with_progress(self.transfer.partial.calc_progress()) .build(); self.view.update(COMPONENT_PROGRESS_BAR_PARTIAL, props) @@ -889,7 +924,6 @@ impl FileTransferActivity { match self.view.get_props(COMPONENT_EXPLORER_FIND) { None => None, Some(props) => { - let title: String = props.texts.title.clone().unwrap_or_default(); // Prepare files let files: Vec = self .found() @@ -897,9 +931,7 @@ impl FileTransferActivity { .iter_files() .map(|x: &FsEntry| self.found().unwrap().fmt_file(x)) .collect(); - let props = FileListPropsBuilder::from(props) - .with_files(Some(title), files) - .build(); + let props = FileListPropsBuilder::from(props).with_files(files).build(); self.view.update(COMPONENT_EXPLORER_FIND, props) } } diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index e9046a5..840345b 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -32,7 +32,6 @@ use crate::fs::FsEntry; use crate::ui::components::{ file_list::{FileList, FileListPropsBuilder}, logbox::{LogBox, LogboxPropsBuilder}, - msgbox::{MsgBox, MsgBoxPropsBuilder}, }; use crate::ui::store::Store; use crate::utils::fmt::fmt_time; @@ -40,15 +39,16 @@ use crate::utils::ui::draw_area_in; // Ext use bytesize::ByteSize; use std::path::PathBuf; -use tuirealm::components::{ +use tui_realm_stdlib::{ input::{Input, InputPropsBuilder}, + list::{List, ListPropsBuilder}, + paragraph::{Paragraph, ParagraphPropsBuilder}, progress_bar::{ProgressBar, ProgressBarPropsBuilder}, radio::{Radio, RadioPropsBuilder}, - scrolltable::{ScrollTablePropsBuilder, Scrolltable}, span::{Span, SpanPropsBuilder}, table::{Table, TablePropsBuilder}, }; -use tuirealm::props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder}; +use tuirealm::props::{Alignment, PropsBuilder, TableBuilder, TextSpan}; use tuirealm::tui::{ layout::{Constraint, Direction, Layout}, style::Color, @@ -101,6 +101,7 @@ impl FileTransferActivity { super::COMPONENT_LOG_BOX, Box::new(LogBox::new( LogboxPropsBuilder::default() + .with_title("Log", Alignment::Left) .with_background(log_background) .with_borders(Borders::ALL, BorderType::Plain, log_panel) .build(), @@ -383,12 +384,13 @@ impl FileTransferActivity { let error_color = self.theme().misc_error_dialog; self.view.mount( super::COMPONENT_TEXT_ERROR, - Box::new(MsgBox::new( - MsgBoxPropsBuilder::default() + Box::new(Paragraph::new( + ParagraphPropsBuilder::default() .with_foreground(error_color) .with_borders(Borders::ALL, BorderType::Rounded, error_color) .bold() - .with_texts(None, vec![TextSpan::from(text)]) + .with_text_alignment(Alignment::Center) + .with_texts(vec![TextSpan::from(text)]) .build(), )), ); @@ -408,12 +410,13 @@ impl FileTransferActivity { let error_color = self.theme().misc_error_dialog; self.view.mount( super::COMPONENT_TEXT_FATAL, - Box::new(MsgBox::new( - MsgBoxPropsBuilder::default() + Box::new(Paragraph::new( + ParagraphPropsBuilder::default() .with_foreground(error_color) .with_borders(Borders::ALL, BorderType::Rounded, error_color) .bold() - .with_texts(None, vec![TextSpan::from(text)]) + .with_text_alignment(Alignment::Center) + .with_texts(vec![TextSpan::from(text)]) .build(), )), ); @@ -422,28 +425,26 @@ impl FileTransferActivity { } pub(super) fn mount_wait(&mut self, text: &str) { - self.mount_wait_ex(text, false, Color::Reset); + self.mount_wait_ex(text, Color::Reset); } pub(super) fn mount_blocking_wait(&mut self, text: &str) { - self.mount_wait_ex(text, true, Color::Reset); + self.mount_wait_ex(text, Color::Reset); self.view(); } - fn mount_wait_ex(&mut self, text: &str, blink: bool, color: Color) { + fn mount_wait_ex(&mut self, text: &str, color: Color) { // Mount - let mut builder: MsgBoxPropsBuilder = MsgBoxPropsBuilder::default(); + let mut builder: ParagraphPropsBuilder = ParagraphPropsBuilder::default(); builder .with_foreground(color) .with_borders(Borders::ALL, BorderType::Rounded, Color::White) .bold() - .with_texts(None, vec![TextSpan::from(text)]); - if blink { - builder.blink(); - } + .with_text_alignment(Alignment::Center) + .with_texts(vec![TextSpan::from(text)]); self.view.mount( super::COMPONENT_TEXT_WAIT, - Box::new(MsgBox::new(builder.build())), + Box::new(Paragraph::new(builder.build())), ); // Give focus to info self.view.active(super::COMPONENT_TEXT_WAIT); @@ -466,10 +467,9 @@ impl FileTransferActivity { .with_color(quit_color) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, quit_color) - .with_options( - Some(String::from("Are you sure you want to quit?")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Are you sure you want to quit?", Alignment::Center) + .with_options(&[String::from("Yes"), String::from("No")]) + .rewind(true) .build(), )), ); @@ -496,10 +496,9 @@ impl FileTransferActivity { .with_color(quit_color) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, quit_color) - .with_options( - Some(String::from("Are you sure you want to disconnect?")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Are you sure you want to disconnect?", Alignment::Center) + .with_options(&[String::from("Yes"), String::from("No")]) + .rewind(true) .build(), )), ); @@ -521,7 +520,7 @@ impl FileTransferActivity { InputPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, input_color) .with_foreground(input_color) - .with_label(String::from("Copy file(s) to…")) + .with_label("Copy file(s) to…", Alignment::Center) .build(), )), ); @@ -540,7 +539,7 @@ impl FileTransferActivity { InputPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, input_color) .with_foreground(input_color) - .with_label(String::from("Execute command")) + .with_label("Execute command", Alignment::Center) .build(), )), ); @@ -570,7 +569,10 @@ impl FileTransferActivity { super::COMPONENT_EXPLORER_FIND, Box::new(FileList::new( FileListPropsBuilder::default() - .with_files(Some(format!("Search results for \"{}\"", search)), vec![]) + .with_title( + format!("Search results for \"{}\"", search), + Alignment::Left, + ) .with_borders(Borders::ALL, BorderType::Plain, hg) .with_highlight_color(hg) .with_background(bg) @@ -594,7 +596,7 @@ impl FileTransferActivity { InputPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, input_color) .with_foreground(input_color) - .with_label(String::from("Search files by name")) + .with_label("Search files by name", Alignment::Center) .build(), )), ); @@ -615,7 +617,7 @@ impl FileTransferActivity { InputPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, input_color) .with_foreground(input_color) - .with_label(String::from("Change working directory")) + .with_label("Change working directory", Alignment::Center) .build(), )), ); @@ -634,7 +636,7 @@ impl FileTransferActivity { InputPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, input_color) .with_foreground(input_color) - .with_label(String::from("Insert directory name")) + .with_label("Insert directory name", Alignment::Center) .build(), )), ); @@ -653,7 +655,7 @@ impl FileTransferActivity { InputPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, input_color) .with_foreground(input_color) - .with_label(String::from("New file name")) + .with_label("New file name", Alignment::Center) .build(), )), ); @@ -672,7 +674,7 @@ impl FileTransferActivity { InputPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, input_color) .with_foreground(input_color) - .with_label(String::from("Open file with…")) + .with_label("Open file with…", Alignment::Center) .build(), )), ); @@ -691,7 +693,7 @@ impl FileTransferActivity { InputPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, input_color) .with_foreground(input_color) - .with_label(String::from("Move file(s) to…")) + .with_label("Move file(s) to…", Alignment::Center) .build(), )), ); @@ -710,7 +712,7 @@ impl FileTransferActivity { InputPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, input_color) .with_foreground(input_color) - .with_label(String::from("Save as…")) + .with_label("Save as…", Alignment::Center) .build(), )), ); @@ -735,7 +737,7 @@ impl FileTransferActivity { BorderType::Rounded, Color::Reset, ) - .with_texts(Some(root_name), String::new()) + .with_title(root_name, Alignment::Center) .build(), )), ); @@ -750,7 +752,7 @@ impl FileTransferActivity { BorderType::Rounded, Color::Reset, ) - .with_texts(Some(String::from("Please wait")), String::new()) + .with_title("Please wait", Alignment::Center) .build(), )), ); @@ -770,10 +772,10 @@ impl FileTransferActivity { _ => panic!("You can't mount file sorting when in found result"), }; let index: usize = match sorting { - FileSorting::ByCreationTime => 2, - FileSorting::ByModifyTime => 1, - FileSorting::ByName => 0, - FileSorting::BySize => 3, + FileSorting::CreationTime => 2, + FileSorting::ModifyTime => 1, + FileSorting::Name => 0, + FileSorting::Size => 3, }; self.view.mount( super::COMPONENT_RADIO_SORTING, @@ -782,15 +784,13 @@ impl FileTransferActivity { .with_color(sorting_color) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, sorting_color) - .with_options( - Some(String::from("Sort files by")), - vec![ - String::from("Name"), - String::from("Modify time"), - String::from("Creation time"), - String::from("Size"), - ], - ) + .with_title("Sort files by", Alignment::Center) + .with_options(&[ + String::from("Name"), + String::from("Modify time"), + String::from("Creation time"), + String::from("Size"), + ]) .with_value(index) .build(), )), @@ -811,11 +811,10 @@ impl FileTransferActivity { .with_color(warn_color) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Plain, warn_color) - .with_options( - Some(String::from("Delete file")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Delete file", Alignment::Center) + .with_options(&[String::from("Yes"), String::from("No")]) .with_value(1) + .rewind(true) .build(), )), ); @@ -841,54 +840,35 @@ impl FileTransferActivity { None => format!("{}", file.get_abs_path().display()), }; // Make texts - texts.add_col(TextSpan::from("Path: ")).add_col( - TextSpanBuilder::new(path.as_str()) - .with_foreground(Color::Yellow) - .build(), - ); + texts + .add_col(TextSpan::from("Path: ")) + .add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); if let Some(filetype) = file.get_ftype() { texts .add_row() .add_col(TextSpan::from("File type: ")) - .add_col( - TextSpanBuilder::new(filetype.as_str()) - .with_foreground(Color::LightGreen) - .build(), - ); + .add_col(TextSpan::new(filetype.as_str()).fg(Color::LightGreen)); } let (bsize, size): (ByteSize, usize) = (ByteSize(file.get_size() as u64), file.get_size()); - texts.add_row().add_col(TextSpan::from("Size: ")).add_col( - TextSpanBuilder::new(format!("{} ({})", bsize, size).as_str()) - .with_foreground(Color::Cyan) - .build(), - ); + texts + .add_row() + .add_col(TextSpan::from("Size: ")) + .add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan)); let ctime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S"); let atime: String = fmt_time(file.get_last_access_time(), "%b %d %Y %H:%M:%S"); let mtime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S"); texts .add_row() .add_col(TextSpan::from("Creation time: ")) - .add_col( - TextSpanBuilder::new(ctime.as_str()) - .with_foreground(Color::LightGreen) - .build(), - ); + .add_col(TextSpan::new(ctime.as_str()).fg(Color::LightGreen)); texts .add_row() .add_col(TextSpan::from("Last modified time: ")) - .add_col( - TextSpanBuilder::new(mtime.as_str()) - .with_foreground(Color::LightBlue) - .build(), - ); + .add_col(TextSpan::new(mtime.as_str()).fg(Color::LightBlue)); texts .add_row() .add_col(TextSpan::from("Last access time: ")) - .add_col( - TextSpanBuilder::new(atime.as_str()) - .with_foreground(Color::LightRed) - .build(), - ); + .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); // User #[cfg(target_family = "unix")] let username: String = match file.get_user() { @@ -911,22 +891,21 @@ impl FileTransferActivity { }; #[cfg(target_os = "windows")] let group: String = format!("{}", file.get_group().unwrap_or(0)); - texts.add_row().add_col(TextSpan::from("User: ")).add_col( - TextSpanBuilder::new(username.as_str()) - .with_foreground(Color::LightYellow) - .build(), - ); - texts.add_row().add_col(TextSpan::from("Group: ")).add_col( - TextSpanBuilder::new(group.as_str()) - .with_foreground(Color::Blue) - .build(), - ); + texts + .add_row() + .add_col(TextSpan::from("User: ")) + .add_col(TextSpan::new(username.as_str()).fg(Color::LightYellow)); + texts + .add_row() + .add_col(TextSpan::from("Group: ")) + .add_col(TextSpan::new(group.as_str()).fg(Color::Blue)); self.view.mount( super::COMPONENT_LIST_FILEINFO, Box::new(Table::new( TablePropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, Color::White) - .with_table(Some(file.get_name().to_string()), texts.build()) + .with_title(file.get_name(), Alignment::Left) + .with_table(texts.build()) .build(), )), ); @@ -941,22 +920,16 @@ impl FileTransferActivity { let sorting_color = self.theme().transfer_status_sorting; let hidden_color = self.theme().transfer_status_hidden; let local_bar_spans: Vec = vec![ - TextSpanBuilder::new("File sorting: ") - .with_foreground(sorting_color) - .build(), - TextSpanBuilder::new(Self::get_file_sorting_str(self.local().get_file_sorting())) - .with_foreground(sorting_color) - .reversed() - .build(), - TextSpanBuilder::new(" Hidden files: ") - .with_foreground(hidden_color) - .build(), - TextSpanBuilder::new(Self::get_hidden_files_str( + TextSpan::new("File sorting: ").fg(sorting_color), + TextSpan::new(Self::get_file_sorting_str(self.local().get_file_sorting())) + .fg(sorting_color) + .reversed(), + TextSpan::new(" Hidden files: ").fg(hidden_color), + TextSpan::new(Self::get_hidden_files_str( self.local().hidden_files_visible(), )) - .with_foreground(hidden_color) - .reversed() - .build(), + .fg(hidden_color) + .reversed(), ]; if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR_LOCAL) { self.view.update( @@ -973,32 +946,23 @@ impl FileTransferActivity { let hidden_color = self.theme().transfer_status_hidden; let sync_color = self.theme().transfer_status_sync_browsing; let remote_bar_spans: Vec = vec![ - TextSpanBuilder::new("File sorting: ") - .with_foreground(sorting_color) - .build(), - TextSpanBuilder::new(Self::get_file_sorting_str(self.remote().get_file_sorting())) - .with_foreground(sorting_color) - .reversed() - .build(), - TextSpanBuilder::new(" Hidden files: ") - .with_foreground(hidden_color) - .build(), - TextSpanBuilder::new(Self::get_hidden_files_str( + TextSpan::new("File sorting: ").fg(sorting_color), + TextSpan::new(Self::get_file_sorting_str(self.remote().get_file_sorting())) + .fg(sorting_color) + .reversed(), + TextSpan::new(" Hidden files: ").fg(hidden_color), + TextSpan::new(Self::get_hidden_files_str( self.remote().hidden_files_visible(), )) - .with_foreground(hidden_color) - .reversed() - .build(), - TextSpanBuilder::new(" Sync Browsing: ") - .with_foreground(sync_color) - .build(), - TextSpanBuilder::new(match self.browser.sync_browsing { + .fg(hidden_color) + .reversed(), + TextSpan::new(" Sync Browsing: ").fg(sync_color), + TextSpan::new(match self.browser.sync_browsing { true => "ON ", false => "OFF", }) - .with_foreground(sync_color) - .reversed() - .build(), + .fg(sync_color) + .reversed(), ]; if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR_REMOTE) { self.view.update( @@ -1017,253 +981,109 @@ impl FileTransferActivity { let key_color = self.theme().misc_keys; self.view.mount( super::COMPONENT_TEXT_HELP, - Box::new(Scrolltable::new( - ScrollTablePropsBuilder::default() + Box::new(List::new( + ListPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, Color::White) .with_highlighted_str(Some("?")) .with_max_scroll_step(8) .bold() - .with_table( - Some(String::from("Help")), + .scrollable(true) + .with_title("Help", Alignment::Center) + .with_rows( TableBuilder::default() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Disconnect")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Switch between explorer and logs", )) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Go to previous directory")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Change explorer tab")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Move up/down in list")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Enter directory")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Upload/Download file")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Toggle hidden files")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Change file sorting mode")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Copy")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Make directory")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Go to path")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Show help")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Show info about selected file")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Reload directory content")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Select file")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Create new file")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Open text file with preferred editor", )) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Quit termscp")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Rename file")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Save file as")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Go to parent directory")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Open file with default application for file type", )) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from( " Open file with specified application", )) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Execute shell command")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Toggle synchronized browsing")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Delete selected file")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Select all files")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(key_color) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(key_color)) .add_col(TextSpan::from(" Interrupt file transfer")) .build(), ) @@ -1280,10 +1100,10 @@ impl FileTransferActivity { fn get_file_sorting_str(mode: FileSorting) -> &'static str { match mode { - FileSorting::ByName => "By name", - FileSorting::ByCreationTime => "By creation time", - FileSorting::ByModifyTime => "By modify time", - FileSorting::BySize => "By size", + FileSorting::Name => "By name", + FileSorting::CreationTime => "By creation time", + FileSorting::ModifyTime => "By modify time", + FileSorting::Size => "By size", } } diff --git a/src/ui/activities/setup/mod.rs b/src/ui/activities/setup/mod.rs index 1a07b39..d615189 100644 --- a/src/ui/activities/setup/mod.rs +++ b/src/ui/activities/setup/mod.rs @@ -152,7 +152,7 @@ impl SetupActivity { } fn config(&self) -> &ConfigClient { - &self.context().config() + self.context().config() } fn config_mut(&mut self) -> &mut ConfigClient { diff --git a/src/ui/activities/setup/update.rs b/src/ui/activities/setup/update.rs index 0f70114..06593e8 100644 --- a/src/ui/activities/setup/update.rs +++ b/src/ui/activities/setup/update.rs @@ -74,65 +74,67 @@ impl SetupActivity { None => None, Some(msg) => match msg { // Input field - (COMPONENT_INPUT_TEXT_EDITOR, &MSG_KEY_DOWN) => { + (COMPONENT_INPUT_TEXT_EDITOR, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_RADIO_DEFAULT_PROTOCOL); None } - (COMPONENT_RADIO_DEFAULT_PROTOCOL, &MSG_KEY_DOWN) => { + (COMPONENT_RADIO_DEFAULT_PROTOCOL, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_RADIO_HIDDEN_FILES); None } - (COMPONENT_RADIO_HIDDEN_FILES, &MSG_KEY_DOWN) => { + (COMPONENT_RADIO_HIDDEN_FILES, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_RADIO_UPDATES); None } - (COMPONENT_RADIO_UPDATES, &MSG_KEY_DOWN) => { + (COMPONENT_RADIO_UPDATES, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_RADIO_GROUP_DIRS); None } - (COMPONENT_RADIO_GROUP_DIRS, &MSG_KEY_DOWN) => { + (COMPONENT_RADIO_GROUP_DIRS, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT); None } - (COMPONENT_INPUT_LOCAL_FILE_FMT, &MSG_KEY_DOWN) => { + (COMPONENT_INPUT_LOCAL_FILE_FMT, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT); None } - (COMPONENT_INPUT_REMOTE_FILE_FMT, &MSG_KEY_DOWN) => { + (COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_INPUT_TEXT_EDITOR); None } // Input field - (COMPONENT_INPUT_REMOTE_FILE_FMT, &MSG_KEY_UP) => { + (COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT); None } - (COMPONENT_INPUT_LOCAL_FILE_FMT, &MSG_KEY_UP) => { + (COMPONENT_INPUT_LOCAL_FILE_FMT, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_RADIO_GROUP_DIRS); None } - (COMPONENT_RADIO_GROUP_DIRS, &MSG_KEY_UP) => { + (COMPONENT_RADIO_GROUP_DIRS, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_RADIO_UPDATES); None } - (COMPONENT_RADIO_UPDATES, &MSG_KEY_UP) => { + (COMPONENT_RADIO_UPDATES, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_RADIO_HIDDEN_FILES); None } - (COMPONENT_RADIO_HIDDEN_FILES, &MSG_KEY_UP) => { + (COMPONENT_RADIO_HIDDEN_FILES, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_RADIO_DEFAULT_PROTOCOL); None } - (COMPONENT_RADIO_DEFAULT_PROTOCOL, &MSG_KEY_UP) => { + (COMPONENT_RADIO_DEFAULT_PROTOCOL, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_INPUT_TEXT_EDITOR); None } - (COMPONENT_INPUT_TEXT_EDITOR, &MSG_KEY_UP) => { + (COMPONENT_INPUT_TEXT_EDITOR, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT); None } // Error or - (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) => { + (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) + if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => + { // Umount text error self.umount_error(); None @@ -161,7 +163,9 @@ impl SetupActivity { } (COMPONENT_RADIO_QUIT, _) => None, // Close help - (COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => { + (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) + if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => + { // Umount help self.umount_help(); None @@ -189,12 +193,12 @@ impl SetupActivity { None } // Show help - (_, &MSG_KEY_CTRL_H) => { + (_, key) if key == &MSG_KEY_CTRL_H => { // Show help self.mount_help(); None } - (_, &MSG_KEY_TAB) => { + (_, key) if key == &MSG_KEY_TAB => { // Change view if let Err(err) = self.action_change_tab(ViewLayout::SshKeys) { self.mount_error(err.as_str()); @@ -202,7 +206,7 @@ impl SetupActivity { None } // Revert changes - (_, &MSG_KEY_CTRL_R) => { + (_, key) if key == &MSG_KEY_CTRL_R => { // Revert changes if let Err(err) = self.action_reset_config() { self.mount_error(err.as_str()); @@ -210,13 +214,13 @@ impl SetupActivity { None } // Save - (_, &MSG_KEY_CTRL_S) => { + (_, key) if key == &MSG_KEY_CTRL_S => { // Show save self.mount_save_popup(); None } // - (_, &MSG_KEY_ESC) => { + (_, key) if key == &MSG_KEY_ESC => { self.action_on_esc(); None } @@ -232,7 +236,9 @@ impl SetupActivity { None => None, Some(msg) => match msg { // Error or - (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) => { + (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) + if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => + { // Umount text error self.umount_error(); None @@ -261,7 +267,9 @@ impl SetupActivity { } (COMPONENT_RADIO_QUIT, _) => None, // Close help - (COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => { + (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) + if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => + { // Umount help self.umount_help(); None @@ -300,28 +308,30 @@ impl SetupActivity { (COMPONENT_RADIO_SAVE, _) => None, // Edit SSH Key // Show help - (_, &MSG_KEY_CTRL_H) => { + (_, key) if key == &MSG_KEY_CTRL_H => { // Show help self.mount_help(); None } // New key - (COMPONENT_INPUT_SSH_HOST, &MSG_KEY_DOWN) => { + (COMPONENT_INPUT_SSH_HOST, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_INPUT_SSH_USERNAME); None } - (COMPONENT_INPUT_SSH_USERNAME, &MSG_KEY_DOWN) => { + (COMPONENT_INPUT_SSH_USERNAME, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_INPUT_SSH_HOST); None } // New key - (COMPONENT_INPUT_SSH_USERNAME, &MSG_KEY_UP) - | (COMPONENT_INPUT_SSH_USERNAME, &MSG_KEY_TAB) => { + (COMPONENT_INPUT_SSH_USERNAME, key) | (COMPONENT_INPUT_SSH_USERNAME, key) + if key == &MSG_KEY_UP || key == &MSG_KEY_TAB => + { self.view.active(COMPONENT_INPUT_SSH_HOST); None } - (COMPONENT_INPUT_SSH_HOST, &MSG_KEY_UP) - | (COMPONENT_INPUT_SSH_HOST, &MSG_KEY_TAB) => { + (COMPONENT_INPUT_SSH_HOST, key) | (COMPONENT_INPUT_SSH_HOST, key) + if key == &MSG_KEY_UP || key == &MSG_KEY_TAB => + { self.view.active(COMPONENT_INPUT_SSH_USERNAME); None } @@ -335,14 +345,15 @@ impl SetupActivity { None } // New key - (COMPONENT_INPUT_SSH_HOST, &MSG_KEY_ESC) - | (COMPONENT_INPUT_SSH_USERNAME, &MSG_KEY_ESC) => { + (COMPONENT_INPUT_SSH_HOST, key) | (COMPONENT_INPUT_SSH_USERNAME, key) + if key == &MSG_KEY_ESC => + { // Umount new ssh key self.umount_new_ssh_key(); None } // New key - (COMPONENT_LIST_SSH_KEYS, &MSG_KEY_CTRL_N) => { + (COMPONENT_LIST_SSH_KEYS, key) if key == &MSG_KEY_CTRL_N => { // Show new key popup self.mount_new_ssh_key(); None @@ -356,13 +367,14 @@ impl SetupActivity { None } // Show delete - (COMPONENT_LIST_SSH_KEYS, &MSG_KEY_CTRL_E) - | (COMPONENT_LIST_SSH_KEYS, &MSG_KEY_DEL) => { + (COMPONENT_LIST_SSH_KEYS, key) | (COMPONENT_LIST_SSH_KEYS, key) + if key == &MSG_KEY_CTRL_E || key == &MSG_KEY_DEL => + { // Show delete key self.mount_del_ssh_key(); None } - (_, &MSG_KEY_TAB) => { + (_, key) if key == &MSG_KEY_TAB => { // Change view if let Err(err) = self.action_change_tab(ViewLayout::Theme) { self.mount_error(err.as_str()); @@ -370,7 +382,7 @@ impl SetupActivity { None } // Revert changes - (_, &MSG_KEY_CTRL_R) => { + (_, key) if key == &MSG_KEY_CTRL_R => { // Revert changes if let Err(err) = self.action_reset_config() { self.mount_error(err.as_str()); @@ -378,13 +390,13 @@ impl SetupActivity { None } // Save - (_, &MSG_KEY_CTRL_S) => { + (_, key) if key == &MSG_KEY_CTRL_S => { // Show save self.mount_save_popup(); None } // - (_, &MSG_KEY_ESC) => { + (_, key) if key == &MSG_KEY_ESC => { self.action_on_esc(); None } @@ -400,217 +412,217 @@ impl SetupActivity { None => None, Some(msg) => match msg { // Input fields - (COMPONENT_COLOR_AUTH_PROTOCOL, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_AUTH_PROTOCOL, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_AUTH_ADDR); None } - (COMPONENT_COLOR_AUTH_ADDR, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_AUTH_ADDR, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_AUTH_PORT); None } - (COMPONENT_COLOR_AUTH_PORT, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_AUTH_PORT, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_AUTH_USERNAME); None } - (COMPONENT_COLOR_AUTH_USERNAME, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_AUTH_USERNAME, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_AUTH_PASSWORD); None } - (COMPONENT_COLOR_AUTH_PASSWORD, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_AUTH_PASSWORD, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_AUTH_BOOKMARKS); None } - (COMPONENT_COLOR_AUTH_BOOKMARKS, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_AUTH_BOOKMARKS, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_AUTH_RECENTS); None } - (COMPONENT_COLOR_AUTH_RECENTS, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_AUTH_RECENTS, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_MISC_ERROR); None } - (COMPONENT_COLOR_MISC_ERROR, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_MISC_ERROR, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_MISC_INPUT); None } - (COMPONENT_COLOR_MISC_INPUT, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_MISC_INPUT, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_MISC_KEYS); None } - (COMPONENT_COLOR_MISC_KEYS, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_MISC_KEYS, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_MISC_QUIT); None } - (COMPONENT_COLOR_MISC_QUIT, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_MISC_QUIT, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_MISC_SAVE); None } - (COMPONENT_COLOR_MISC_SAVE, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_MISC_SAVE, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_MISC_WARN); None } - (COMPONENT_COLOR_MISC_WARN, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_MISC_WARN, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, key) if key == &MSG_KEY_DOWN => { self.view .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, key) if key == &MSG_KEY_DOWN => { self.view .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, key) if key == &MSG_KEY_DOWN => { self.view .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL); None } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL); None } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_TRANSFER_LOG_BG); None } - (COMPONENT_COLOR_TRANSFER_LOG_BG, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_LOG_BG, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_TRANSFER_LOG_WIN); None } - (COMPONENT_COLOR_TRANSFER_LOG_WIN, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_LOG_WIN, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SORTING); None } - (COMPONENT_COLOR_TRANSFER_STATUS_SORTING, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_STATUS_SORTING, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN); None } - (COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SYNC); None } - (COMPONENT_COLOR_TRANSFER_STATUS_SYNC, &MSG_KEY_DOWN) => { + (COMPONENT_COLOR_TRANSFER_STATUS_SYNC, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_COLOR_AUTH_PROTOCOL); None } - (COMPONENT_COLOR_AUTH_PROTOCOL, &MSG_KEY_UP) => { + (COMPONENT_COLOR_AUTH_PROTOCOL, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SYNC); None } - (COMPONENT_COLOR_AUTH_ADDR, &MSG_KEY_UP) => { + (COMPONENT_COLOR_AUTH_ADDR, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_AUTH_PROTOCOL); None } - (COMPONENT_COLOR_AUTH_PORT, &MSG_KEY_UP) => { + (COMPONENT_COLOR_AUTH_PORT, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_AUTH_ADDR); None } - (COMPONENT_COLOR_AUTH_USERNAME, &MSG_KEY_UP) => { + (COMPONENT_COLOR_AUTH_USERNAME, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_AUTH_PORT); None } - (COMPONENT_COLOR_AUTH_PASSWORD, &MSG_KEY_UP) => { + (COMPONENT_COLOR_AUTH_PASSWORD, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_AUTH_USERNAME); None } - (COMPONENT_COLOR_AUTH_BOOKMARKS, &MSG_KEY_UP) => { + (COMPONENT_COLOR_AUTH_BOOKMARKS, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_AUTH_PASSWORD); None } - (COMPONENT_COLOR_AUTH_RECENTS, &MSG_KEY_UP) => { + (COMPONENT_COLOR_AUTH_RECENTS, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_AUTH_BOOKMARKS); None } - (COMPONENT_COLOR_MISC_ERROR, &MSG_KEY_UP) => { + (COMPONENT_COLOR_MISC_ERROR, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_AUTH_RECENTS); None } - (COMPONENT_COLOR_MISC_INPUT, &MSG_KEY_UP) => { + (COMPONENT_COLOR_MISC_INPUT, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_MISC_ERROR); None } - (COMPONENT_COLOR_MISC_KEYS, &MSG_KEY_UP) => { + (COMPONENT_COLOR_MISC_KEYS, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_MISC_INPUT); None } - (COMPONENT_COLOR_MISC_QUIT, &MSG_KEY_UP) => { + (COMPONENT_COLOR_MISC_QUIT, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_MISC_KEYS); None } - (COMPONENT_COLOR_MISC_SAVE, &MSG_KEY_UP) => { + (COMPONENT_COLOR_MISC_SAVE, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_MISC_QUIT); None } - (COMPONENT_COLOR_MISC_WARN, &MSG_KEY_UP) => { + (COMPONENT_COLOR_MISC_WARN, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_MISC_SAVE); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_MISC_WARN); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, key) if key == &MSG_KEY_UP => { self.view .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG); None } - (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, key) if key == &MSG_KEY_UP => { self.view .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG); None } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, key) if key == &MSG_KEY_UP => { self.view .active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG); None } - (COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL); None } - (COMPONENT_COLOR_TRANSFER_LOG_BG, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_LOG_BG, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL); None } - (COMPONENT_COLOR_TRANSFER_LOG_WIN, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_LOG_WIN, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_TRANSFER_LOG_BG); None } - (COMPONENT_COLOR_TRANSFER_STATUS_SORTING, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_STATUS_SORTING, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_TRANSFER_LOG_WIN); None } - (COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SORTING); None } - (COMPONENT_COLOR_TRANSFER_STATUS_SYNC, &MSG_KEY_UP) => { + (COMPONENT_COLOR_TRANSFER_STATUS_SYNC, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN); None } @@ -624,7 +636,9 @@ impl SetupActivity { None } // Error or - (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) => { + (COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key) + if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => + { // Umount text error self.umount_error(); None @@ -653,7 +667,9 @@ impl SetupActivity { } (COMPONENT_RADIO_QUIT, _) => None, // Close help - (COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => { + (COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key) + if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => + { // Umount help self.umount_help(); None @@ -676,12 +692,12 @@ impl SetupActivity { (COMPONENT_RADIO_SAVE, _) => None, // Edit SSH Key // Show help - (_, &MSG_KEY_CTRL_H) => { + (_, key) if key == &MSG_KEY_CTRL_H => { // Show help self.mount_help(); None } - (_, &MSG_KEY_TAB) => { + (_, key) if key == &MSG_KEY_TAB => { // Change view if let Err(err) = self.action_change_tab(ViewLayout::SetupForm) { self.mount_error(err.as_str()); @@ -689,7 +705,7 @@ impl SetupActivity { None } // Revert changes - (_, &MSG_KEY_CTRL_R) => { + (_, key) if key == &MSG_KEY_CTRL_R => { // Revert changes if let Err(err) = self.action_reset_theme() { self.mount_error(err.as_str()); @@ -697,13 +713,13 @@ impl SetupActivity { None } // Save - (_, &MSG_KEY_CTRL_S) => { + (_, key) if key == &MSG_KEY_CTRL_S => { // Show save self.mount_save_popup(); None } // - (_, &MSG_KEY_ESC) => { + (_, key) if key == &MSG_KEY_ESC => { self.action_on_esc(); None } diff --git a/src/ui/activities/setup/view/mod.rs b/src/ui/activities/setup/view/mod.rs index a4b6784..994ae8f 100644 --- a/src/ui/activities/setup/view/mod.rs +++ b/src/ui/activities/setup/view/mod.rs @@ -34,14 +34,14 @@ use super::*; pub use setup::*; pub use ssh_keys::*; pub use theme::*; -// Locals -use crate::ui::components::msgbox::{MsgBox, MsgBoxPropsBuilder}; // Ext -use tuirealm::components::{ +use tui_realm_stdlib::{ + list::{List, ListPropsBuilder}, + paragraph::{Paragraph, ParagraphPropsBuilder}, radio::{Radio, RadioPropsBuilder}, - scrolltable::{ScrollTablePropsBuilder, Scrolltable}, + span::{Span, SpanPropsBuilder}, }; -use tuirealm::props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder}; +use tuirealm::props::{Alignment, PropsBuilder, TableBuilder, TextSpan}; use tuirealm::tui::{ style::Color, widgets::{BorderType, Borders}, @@ -79,12 +79,13 @@ impl SetupActivity { // Mount self.view.mount( super::COMPONENT_TEXT_ERROR, - Box::new(MsgBox::new( - MsgBoxPropsBuilder::default() + Box::new(Paragraph::new( + ParagraphPropsBuilder::default() .with_foreground(Color::Red) .bold() .with_borders(Borders::ALL, BorderType::Rounded, Color::Red) - .with_texts(None, vec![TextSpan::from(text)]) + .with_texts(vec![TextSpan::from(text)]) + .with_text_alignment(Alignment::Center) .build(), )), ); @@ -110,16 +111,16 @@ impl SetupActivity { .with_color(Color::LightRed) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed) - .with_options( - Some(String::from( - "There are unsaved changes! Save changes before leaving?", - )), - vec![ - String::from("Save"), - String::from("Don't save"), - String::from("Cancel"), - ], + .with_title( + "There are unsaved changes! Save changes before leaving?", + Alignment::Center, ) + .with_options(&[ + String::from("Save"), + String::from("Don't save"), + String::from("Cancel"), + ]) + .rewind(true) .build(), )), ); @@ -145,10 +146,9 @@ impl SetupActivity { .with_color(Color::LightYellow) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow) - .with_options( - Some(String::from("Save changes?")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Save changes?", Alignment::Center) + .with_options(&[String::from("Yes"), String::from("No")]) + .rewind(true) .build(), )), ); @@ -163,91 +163,82 @@ impl SetupActivity { self.view.umount(super::COMPONENT_RADIO_SAVE); } + pub(self) fn mount_header_tab(&mut self, idx: usize) { + self.view.mount( + super::COMPONENT_RADIO_TAB, + Box::new(Radio::new( + RadioPropsBuilder::default() + .with_color(Color::LightYellow) + .with_inverted_color(Color::Black) + .with_borders(Borders::BOTTOM, BorderType::Thick, Color::White) + .with_options(&[ + String::from("User Interface"), + String::from("SSH Keys"), + String::from("Theme"), + ]) + .with_value(idx) + .rewind(true) + .build(), + )), + ); + } + + pub(self) fn mount_footer(&mut self) { + self.view.mount( + super::COMPONENT_TEXT_FOOTER, + Box::new(Span::new( + SpanPropsBuilder::default() + .with_spans(vec![ + TextSpan::new("Press ").bold(), + TextSpan::new("").bold().fg(Color::Cyan), + TextSpan::new(" to show keybindings").bold(), + ]) + .build(), + )), + ); + } + /// ### mount_help /// /// Mount help pub(super) fn mount_help(&mut self) { self.view.mount( super::COMPONENT_TEXT_HELP, - Box::new(Scrolltable::new( - ScrollTablePropsBuilder::default() + Box::new(List::new( + ListPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, Color::White) .with_highlighted_str(Some("?")) .with_max_scroll_step(8) .bold() - .with_table( - Some(String::from("Help")), + .with_title("Help", Alignment::Center) + .scrollable(true) + .with_rows( TableBuilder::default() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) .add_col(TextSpan::from(" Exit setup")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) .add_col(TextSpan::from(" Change setup page")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) .add_col(TextSpan::from(" Change cursor")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) .add_col(TextSpan::from(" Change input field")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) .add_col(TextSpan::from(" Select / Dismiss popup")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) .add_col(TextSpan::from(" Delete SSH key")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) .add_col(TextSpan::from(" New SSH key")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) .add_col(TextSpan::from(" Revert changes")) .add_row() - .add_col( - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - ) + .add_col(TextSpan::new("").bold().fg(Color::Cyan)) .add_col(TextSpan::from(" Save configuration")) .build(), ) diff --git a/src/ui/activities/setup/view/setup.rs b/src/ui/activities/setup/view/setup.rs index 9a8c316..218c8fb 100644 --- a/src/ui/activities/setup/view/setup.rs +++ b/src/ui/activities/setup/view/setup.rs @@ -33,10 +33,9 @@ use crate::fs::explorer::GroupDirs; use crate::utils::ui::draw_area_in; // Ext use std::path::PathBuf; -use tuirealm::components::{ +use tui_realm_stdlib::{ input::{Input, InputPropsBuilder}, radio::{Radio, RadioPropsBuilder}, - span::{Span, SpanPropsBuilder}, }; use tuirealm::tui::{ layout::{Constraint, Direction, Layout}, @@ -44,7 +43,7 @@ use tuirealm::tui::{ widgets::{BorderType, Borders, Clear}, }; use tuirealm::{ - props::{PropsBuilder, TextSpanBuilder}, + props::{Alignment, PropsBuilder}, Payload, Value, View, }; @@ -59,41 +58,9 @@ impl SetupActivity { self.view = View::init(); // Common stuff // Radio tab - self.view.mount( - super::COMPONENT_RADIO_TAB, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(Color::LightYellow) - .with_inverted_color(Color::Black) - .with_borders(Borders::BOTTOM, BorderType::Thick, Color::White) - .with_options( - None, - vec![ - String::from("User Interface"), - String::from("SSH Keys"), - String::from("Theme"), - ], - ) - .with_value(0) - .build(), - )), - ); + self.mount_header_tab(0); // Footer - self.view.mount( - super::COMPONENT_TEXT_FOOTER, - Box::new(Span::new( - SpanPropsBuilder::default() - .with_spans(vec![ - TextSpanBuilder::new("Press ").bold().build(), - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - TextSpanBuilder::new(" to show keybindings").bold().build(), - ]) - .build(), - )), - ); + self.mount_footer(); // Input fields self.view.mount( super::COMPONENT_INPUT_TEXT_EDITOR, @@ -101,7 +68,7 @@ impl SetupActivity { InputPropsBuilder::default() .with_foreground(Color::LightGreen) .with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen) - .with_label(String::from("Text editor")) + .with_label("Text editor", Alignment::Left) .build(), )), ); @@ -113,15 +80,14 @@ impl SetupActivity { .with_color(Color::LightCyan) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan) - .with_options( - Some(String::from("Default file transfer protocol")), - vec![ - String::from("SFTP"), - String::from("SCP"), - String::from("FTP"), - String::from("FTPS"), - ], - ) + .with_title("Default file transfer protocol", Alignment::Left) + .with_options(&[ + String::from("SFTP"), + String::from("SCP"), + String::from("FTP"), + String::from("FTPS"), + ]) + .rewind(true) .build(), )), ); @@ -132,10 +98,9 @@ impl SetupActivity { .with_color(Color::LightRed) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed) - .with_options( - Some(String::from("Show hidden files (by default)")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Show hidden files (by default)?", Alignment::Left) + .with_options(&[String::from("Yes"), String::from("No")]) + .rewind(true) .build(), )), ); @@ -146,10 +111,9 @@ impl SetupActivity { .with_color(Color::LightYellow) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow) - .with_options( - Some(String::from("Check for updates?")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Check for updates?", Alignment::Left) + .with_options(&[String::from("Yes"), String::from("No")]) + .rewind(true) .build(), )), ); @@ -160,14 +124,13 @@ impl SetupActivity { .with_color(Color::LightMagenta) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta) - .with_options( - Some(String::from("Group directories")), - vec![ - String::from("Display first"), - String::from("Display Last"), - String::from("No"), - ], - ) + .with_title("Group directories", Alignment::Left) + .with_options(&[ + String::from("Display first"), + String::from("Display Last"), + String::from("No"), + ]) + .rewind(true) .build(), )), ); @@ -177,7 +140,7 @@ impl SetupActivity { InputPropsBuilder::default() .with_foreground(Color::LightBlue) .with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue) - .with_label(String::from("File formatter syntax (local)")) + .with_label("File formatter syntax (local)", Alignment::Left) .build(), )), ); @@ -187,7 +150,7 @@ impl SetupActivity { InputPropsBuilder::default() .with_foreground(Color::LightGreen) .with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen) - .with_label(String::from("File formatter syntax (remote)")) + .with_label("File formatter syntax (remote)", Alignment::Left) .build(), )), ); diff --git a/src/ui/activities/setup/view/ssh_keys.rs b/src/ui/activities/setup/view/ssh_keys.rs index 3517178..756c618 100644 --- a/src/ui/activities/setup/view/ssh_keys.rs +++ b/src/ui/activities/setup/view/ssh_keys.rs @@ -31,10 +31,9 @@ use super::{Context, SetupActivity}; use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder}; use crate::utils::ui::draw_area_in; // Ext -use tuirealm::components::{ +use tui_realm_stdlib::{ input::{Input, InputPropsBuilder}, radio::{Radio, RadioPropsBuilder}, - span::{Span, SpanPropsBuilder}, }; use tuirealm::tui::{ layout::{Constraint, Direction, Layout}, @@ -42,7 +41,7 @@ use tuirealm::tui::{ widgets::{BorderType, Borders, Clear}, }; use tuirealm::{ - props::{PropsBuilder, TextSpanBuilder}, + props::{Alignment, PropsBuilder}, View, }; @@ -57,46 +56,15 @@ impl SetupActivity { self.view = View::init(); // Common stuff // Radio tab - self.view.mount( - super::COMPONENT_RADIO_TAB, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(Color::LightYellow) - .with_inverted_color(Color::Black) - .with_borders(Borders::BOTTOM, BorderType::Thick, Color::LightYellow) - .with_options( - None, - vec![ - String::from("User Interface"), - String::from("SSH Keys"), - String::from("Theme"), - ], - ) - .with_value(1) - .build(), - )), - ); + // Radio tab + self.mount_header_tab(1); // Footer - self.view.mount( - super::COMPONENT_TEXT_FOOTER, - Box::new(Span::new( - SpanPropsBuilder::default() - .with_spans(vec![ - TextSpanBuilder::new("Press ").bold().build(), - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - TextSpanBuilder::new(" to show keybindings").bold().build(), - ]) - .build(), - )), - ); + self.mount_footer(); self.view.mount( super::COMPONENT_LIST_SSH_KEYS, Box::new(BookmarkList::new( BookmarkListPropsBuilder::default() - .with_bookmarks(Some(String::from("SSH Keys")), vec![]) + .with_title("SSH keys", Alignment::Left) .with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen) .with_background(Color::LightGreen) .with_foreground(Color::Black) @@ -211,11 +179,10 @@ impl SetupActivity { .with_color(Color::LightRed) .with_inverted_color(Color::Black) .with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed) - .with_options( - Some(String::from("Delete key?")), - vec![String::from("Yes"), String::from("No")], - ) + .with_title("Delete key?", Alignment::Center) + .with_options(&[String::from("Yes"), String::from("No")]) .with_value(1) // Default: No + .rewind(true) .build(), )), ); @@ -238,7 +205,7 @@ impl SetupActivity { super::COMPONENT_INPUT_SSH_HOST, Box::new(Input::new( InputPropsBuilder::default() - .with_label(String::from("Hostname or address")) + .with_label("Hostname or address", Alignment::Center) .with_borders( Borders::TOP | Borders::RIGHT | Borders::LEFT, BorderType::Plain, @@ -251,7 +218,7 @@ impl SetupActivity { super::COMPONENT_INPUT_SSH_USERNAME, Box::new(Input::new( InputPropsBuilder::default() - .with_label(String::from("Username")) + .with_label("Username", Alignment::Center) .with_borders( Borders::BOTTOM | Borders::RIGHT | Borders::LEFT, BorderType::Plain, @@ -287,7 +254,7 @@ impl SetupActivity { }) .collect(); let props = BookmarkListPropsBuilder::from(props) - .with_bookmarks(Some(String::from("SSH Keys")), keys) + .with_bookmarks(keys) .build(); self.view.update(super::COMPONENT_LIST_SSH_KEYS, props); } diff --git a/src/ui/activities/setup/view/theme.rs b/src/ui/activities/setup/view/theme.rs index 5bb092b..e3af5dc 100644 --- a/src/ui/activities/setup/view/theme.rs +++ b/src/ui/activities/setup/view/theme.rs @@ -33,18 +33,14 @@ use crate::ui::components::color_picker::{ColorPicker, ColorPickerPropsBuilder}; use crate::utils::parser::parse_color; use crate::utils::ui::draw_area_in; // Ext -use tuirealm::components::{ - label::{Label, LabelPropsBuilder}, - radio::{Radio, RadioPropsBuilder}, - span::{Span, SpanPropsBuilder}, -}; +use tui_realm_stdlib::label::{Label, LabelPropsBuilder}; use tuirealm::tui::{ layout::{Constraint, Direction, Layout}, style::Color, widgets::{BorderType, Borders, Clear}, }; use tuirealm::{ - props::{PropsBuilder, TextSpanBuilder}, + props::{Alignment, PropsBuilder}, Payload, Value, View, }; @@ -59,41 +55,9 @@ impl SetupActivity { self.view = View::init(); // Common stuff // Radio tab - self.view.mount( - super::COMPONENT_RADIO_TAB, - Box::new(Radio::new( - RadioPropsBuilder::default() - .with_color(Color::LightYellow) - .with_inverted_color(Color::Black) - .with_borders(Borders::BOTTOM, BorderType::Thick, Color::White) - .with_options( - None, - vec![ - String::from("User Interface"), - String::from("SSH Keys"), - String::from("Theme"), - ], - ) - .with_value(2) - .build(), - )), - ); + self.mount_header_tab(2); // Footer - self.view.mount( - super::COMPONENT_TEXT_FOOTER, - Box::new(Span::new( - SpanPropsBuilder::default() - .with_spans(vec![ - TextSpanBuilder::new("Press ").bold().build(), - TextSpanBuilder::new("") - .bold() - .with_foreground(Color::Cyan) - .build(), - TextSpanBuilder::new(" to show keybindings").bold().build(), - ]) - .build(), - )), - ); + self.mount_footer(); // auth colors self.mount_title(super::COMPONENT_COLOR_AUTH_TITLE, "Authentication styles"); self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PROTOCOL, "Protocol"); @@ -653,7 +617,7 @@ impl SetupActivity { Box::new(ColorPicker::new( ColorPickerPropsBuilder::default() .with_borders(Borders::ALL, BorderType::Rounded, Color::Reset) - .with_label(label.to_string()) + .with_label(label.to_string(), Alignment::Left) .build(), )), ); diff --git a/src/ui/components/bookmark_list.rs b/src/ui/components/bookmark_list.rs index a541db7..01d81c0 100644 --- a/src/ui/components/bookmark_list.rs +++ b/src/ui/components/bookmark_list.rs @@ -26,18 +26,19 @@ * SOFTWARE. */ // ext -use tuirealm::components::utils::get_block; +use tui_realm_stdlib::utils::get_block; use tuirealm::event::{Event, KeyCode}; -use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan}; +use tuirealm::props::{Alignment, BlockTitle, BordersProps, Props, PropsBuilder}; use tuirealm::tui::{ layout::{Corner, Rect}, style::{Color, Style}, text::Span, widgets::{BorderType, Borders, List, ListItem, ListState}, }; -use tuirealm::{Canvas, Component, Msg, Payload, Value}; +use tuirealm::{Component, Frame, Msg, Payload, PropPayload, PropValue, Value}; // -- props +const PROP_BOOKMARKS: &str = "bookmarks"; pub struct BookmarkListPropsBuilder { props: Option, @@ -117,10 +118,19 @@ impl BookmarkListPropsBuilder { self } - pub fn with_bookmarks(&mut self, title: Option, bookmarks: Vec) -> &mut Self { + pub fn with_title>(&mut self, text: S, alignment: Alignment) -> &mut Self { if let Some(props) = self.props.as_mut() { - let bookmarks: Vec = bookmarks.into_iter().map(TextSpan::from).collect(); - props.texts = TextParts::new(title, Some(bookmarks)); + props.title = Some(BlockTitle::new(text, alignment)); + } + self + } + + pub fn with_bookmarks(&mut self, bookmarks: Vec) -> &mut Self { + if let Some(props) = self.props.as_mut() { + let bookmarks: Vec = bookmarks.into_iter().map(PropValue::Str).collect(); + props + .own + .insert(PROP_BOOKMARKS, PropPayload::Vec(bookmarks)); } self } @@ -210,25 +220,30 @@ impl BookmarkList { // Initialize states let mut states: OwnStates = OwnStates::default(); // Set list length - states.set_list_len(match &props.texts.spans { - Some(tokens) => tokens.len(), - None => 0, - }); + states.set_list_len(Self::bookmarks_len(&props)); BookmarkList { props, states } } + + fn bookmarks_len(props: &Props) -> usize { + match props.own.get(PROP_BOOKMARKS) { + None => 0, + Some(bookmarks) => bookmarks.unwrap_vec().len(), + } + } } impl Component for BookmarkList { #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Canvas, area: Rect) { + fn render(&self, render: &mut Frame, area: Rect) { if self.props.visible { // Make list - let list_item: Vec = match self.props.texts.spans.as_ref() { - None => vec![], - Some(lines) => lines + let list_item: Vec = match self.props.own.get(PROP_BOOKMARKS) { + Some(PropPayload::Vec(lines)) => lines .iter() - .map(|line| ListItem::new(Span::from(line.content.to_string()))) + .map(|x| x.unwrap_str()) + .map(|x| ListItem::new(Span::from(x.to_string()))) .collect(), + _ => vec![], }; let (fg, bg): (Color, Color) = match self.states.focus { true => (self.props.foreground, self.props.background), @@ -241,7 +256,7 @@ impl Component for BookmarkList { List::new(list_item) .block(get_block( &self.props.borders, - &self.props.texts.title, + self.props.title.as_ref(), self.states.focus, )) .start_corner(Corner::TopLeft) @@ -260,10 +275,7 @@ impl Component for BookmarkList { fn update(&mut self, props: Props) -> Msg { self.props = props; // re-Set list length - self.states.set_list_len(match &self.props.texts.spans { - Some(tokens) => tokens.len(), - None => 0, - }); + self.states.set_list_len(Self::bookmarks_len(&self.props)); // Reset list index self.states.reset_list_index(); Msg::None @@ -347,20 +359,24 @@ mod tests { .with_foreground(Color::Red) .with_background(Color::Blue) .with_borders(Borders::ALL, BorderType::Double, Color::Red) - .with_bookmarks( - Some(String::from("filelist")), - vec![String::from("file1"), String::from("file2")], - ) + .with_title("filelist", Alignment::Left) + .with_bookmarks(vec![String::from("file1"), String::from("file2")]) .build(), ); assert_eq!(component.props.foreground, Color::Red); assert_eq!(component.props.background, Color::Blue); assert_eq!(component.props.visible, true); + assert_eq!(component.props.title.as_ref().unwrap().text(), "filelist"); assert_eq!( - component.props.texts.title.as_ref().unwrap().as_str(), - "filelist" + component + .props + .own + .get(PROP_BOOKMARKS) + .unwrap() + .unwrap_vec() + .len(), + 2 ); - assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2); // Verify states assert_eq!(component.states.list_index, 0); assert_eq!(component.states.list_len, 2); @@ -384,14 +400,11 @@ mod tests { // Update component.update( BookmarkListPropsBuilder::from(component.get_props()) - .with_bookmarks( - Some(String::from("filelist")), - vec![ - String::from("file1"), - String::from("file2"), - String::from("file3"), - ], - ) + .with_bookmarks(vec![ + String::from("file1"), + String::from("file2"), + String::from("file3"), + ]) .build(), ); // Verify states diff --git a/src/ui/components/color_picker.rs b/src/ui/components/color_picker.rs index 2fc9df8..c1ba2bb 100644 --- a/src/ui/components/color_picker.rs +++ b/src/ui/components/color_picker.rs @@ -30,15 +30,15 @@ use crate::utils::fmt::fmt_color; use crate::utils::parser::parse_color; // ext -use tuirealm::components::input::{Input, InputPropsBuilder}; +use tui_realm_stdlib::input::{Input, InputPropsBuilder}; use tuirealm::event::Event; -use tuirealm::props::{Props, PropsBuilder}; +use tuirealm::props::{Alignment, Props, PropsBuilder}; use tuirealm::tui::{ layout::Rect, style::Color, widgets::{BorderType, Borders}, }; -use tuirealm::{Canvas, Component, Msg, Payload, Value}; +use tuirealm::{Component, Frame, Msg, Payload, Value}; // -- props @@ -98,8 +98,8 @@ impl ColorPickerPropsBuilder { /// ### with_label /// /// Set input label - pub fn with_label(&mut self, label: String) -> &mut Self { - self.puppet.with_label(label); + pub fn with_label>(&mut self, label: S, alignment: Alignment) -> &mut Self { + self.puppet.with_label(label, alignment); self } @@ -149,7 +149,7 @@ impl Component for ColorPicker { /// Based on the current properties and states, renders a widget using the provided render engine in the provided Area /// If focused, cursor is also set (if supported by widget) #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Canvas, area: Rect) { + fn render(&self, render: &mut Frame, area: Rect) { self.input.render(render, area); } @@ -260,6 +260,7 @@ mod test { .visible() .with_color(&Color::Rgb(204, 170, 0)) .with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0)) + .with_label("omar", Alignment::Left) .build(), ); // Focus diff --git a/src/ui/components/file_list.rs b/src/ui/components/file_list.rs index a0db059..9600659 100644 --- a/src/ui/components/file_list.rs +++ b/src/ui/components/file_list.rs @@ -26,10 +26,10 @@ * SOFTWARE. */ // ext -use tuirealm::components::utils::get_block; +use tui_realm_stdlib::utils::get_block; use tuirealm::event::{Event, KeyCode, KeyModifiers}; use tuirealm::props::{ - BordersProps, PropPayload, PropValue, Props, PropsBuilder, TextParts, TextSpan, + Alignment, BlockTitle, BordersProps, PropPayload, PropValue, Props, PropsBuilder, }; use tuirealm::tui::{ layout::{Corner, Rect}, @@ -37,11 +37,12 @@ use tuirealm::tui::{ text::Span, widgets::{BorderType, Borders, List, ListItem, ListState}, }; -use tuirealm::{Canvas, Component, Msg, Payload, Value}; +use tuirealm::{Component, Frame, Msg, Payload, Value}; // -- props -const PROP_HIGHLIGHT_COLOR: &str = "props-highlight-color"; +const PROP_FILES: &str = "files"; +const PALETTE_HIGHLIGHT_COLOR: &str = "props-highlight-color"; pub struct FileListPropsBuilder { props: Option, @@ -107,10 +108,7 @@ impl FileListPropsBuilder { /// Set highlighted color pub fn with_highlight_color(&mut self, color: Color) -> &mut Self { if let Some(props) = self.props.as_mut() { - props.own.insert( - PROP_HIGHLIGHT_COLOR, - PropPayload::One(PropValue::Color(color)), - ); + props.palette.insert(PALETTE_HIGHLIGHT_COLOR, color); } self } @@ -134,10 +132,17 @@ impl FileListPropsBuilder { self } - pub fn with_files(&mut self, title: Option, files: Vec) -> &mut Self { + pub fn with_title>(&mut self, text: S, alignment: Alignment) -> &mut Self { if let Some(props) = self.props.as_mut() { - let files: Vec = files.into_iter().map(TextSpan::from).collect(); - props.texts = TextParts::new(title, Some(files)); + props.title = Some(BlockTitle::new(text, alignment)); + } + self + } + + pub fn with_files(&mut self, files: Vec) -> &mut Self { + if let Some(props) = self.props.as_mut() { + let files: Vec = files.into_iter().map(PropValue::Str).collect(); + props.own.insert(PROP_FILES, PropPayload::Vec(files)); } self } @@ -299,32 +304,39 @@ impl FileList { // Initialize states let mut states: OwnStates = OwnStates::default(); // Init list states - states.init_list_states(props.texts.spans.as_ref().map(|x| x.len()).unwrap_or(0)); + states.init_list_states(Self::files_len(&props)); FileList { props, states } } + + fn files_len(props: &Props) -> usize { + match props.own.get(PROP_FILES) { + None => 0, + Some(files) => files.unwrap_vec().len(), + } + } } impl Component for FileList { #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Canvas, area: Rect) { + fn render(&self, render: &mut Frame, area: Rect) { if self.props.visible { // Make list - let list_item: Vec = match self.props.texts.spans.as_ref() { - None => vec![], - Some(lines) => lines + let list_item: Vec = match self.props.own.get(PROP_FILES) { + Some(PropPayload::Vec(lines)) => lines .iter() .enumerate() .map(|(num, line)| { let to_display: String = match self.states.is_selected(num) { - true => format!("*{}", line.content), - false => line.content.to_string(), + true => format!("*{}", line.unwrap_str()), + false => line.unwrap_str().to_string(), }; ListItem::new(Span::from(to_display)) }) .collect(), + _ => vec![], }; - let highlighted_color: Color = match self.props.own.get(PROP_HIGHLIGHT_COLOR) { - Some(PropPayload::One(PropValue::Color(c))) => *c, + let highlighted_color: Color = match self.props.palette.get(PALETTE_HIGHLIGHT_COLOR) { + Some(c) => *c, _ => Color::Reset, }; let (h_fg, h_bg): (Color, Color) = match self.states.focus { @@ -338,7 +350,7 @@ impl Component for FileList { List::new(list_item) .block(get_block( &self.props.borders, - &self.props.texts.title, + self.props.title.as_ref(), self.states.focus, )) .start_corner(Corner::TopLeft) @@ -362,14 +374,7 @@ impl Component for FileList { fn update(&mut self, props: Props) -> Msg { self.props = props; // re-Set list states - self.states.init_list_states( - self.props - .texts - .spans - .as_ref() - .map(|x| x.len()) - .unwrap_or(0), - ); + self.states.init_list_states(Self::files_len(&self.props)); Msg::None } @@ -551,24 +556,33 @@ mod tests { .with_background(Color::Blue) .with_highlight_color(Color::LightRed) .with_borders(Borders::ALL, BorderType::Double, Color::Red) - .with_files( - Some(String::from("files")), - vec![String::from("file1"), String::from("file2")], - ) + .with_title("files", Alignment::Left) + .with_files(vec![String::from("file1"), String::from("file2")]) .build(), ); assert_eq!( - *component.props.own.get(PROP_HIGHLIGHT_COLOR).unwrap(), - PropPayload::One(PropValue::Color(Color::LightRed)) + *component + .props + .palette + .get(PALETTE_HIGHLIGHT_COLOR) + .unwrap(), + Color::LightRed ); assert_eq!(component.props.foreground, Color::Red); assert_eq!(component.props.background, Color::Blue); assert_eq!(component.props.visible, true); + assert_eq!(component.props.title.as_ref().unwrap().text(), "files"); assert_eq!( - component.props.texts.title.as_ref().unwrap().as_str(), - "files" + component + .props + .own + .get(PROP_FILES) + .as_ref() + .unwrap() + .unwrap_vec() + .len(), + 2 ); - assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2); // Verify states assert_eq!(component.states.list_index, 0); assert_eq!(component.states.selected.len(), 0); @@ -594,14 +608,11 @@ mod tests { // Update component.update( FileListPropsBuilder::from(component.get_props()) - .with_files( - Some(String::from("filelist")), - vec![ - String::from("file1"), - String::from("file2"), - String::from("file3"), - ], - ) + .with_files(vec![ + String::from("file1"), + String::from("file2"), + String::from("file3"), + ]) .build(), ); // Verify states @@ -670,14 +681,11 @@ mod tests { // Make component let mut component: FileList = FileList::new( FileListPropsBuilder::default() - .with_files( - Some(String::from("files")), - vec![ - String::from("file1"), - String::from("file2"), - String::from("file3"), - ], - ) + .with_files(vec![ + String::from("file1"), + String::from("file2"), + String::from("file3"), + ]) .build(), ); // Get state @@ -735,10 +743,7 @@ mod tests { // Update files component.update( FileListPropsBuilder::from(component.get_props()) - .with_files( - Some(String::from("filelist")), - vec![String::from("file1"), String::from("file2")], - ) + .with_files(vec![String::from("file1"), String::from("file2")]) .build(), ); // Selection should now be empty diff --git a/src/ui/components/logbox.rs b/src/ui/components/logbox.rs index 8e134a5..41ac7d1 100644 --- a/src/ui/components/logbox.rs +++ b/src/ui/components/logbox.rs @@ -26,18 +26,22 @@ * SOFTWARE. */ // ext -use tuirealm::components::utils::{get_block, wrap_spans}; +use tui_realm_stdlib::utils::{get_block, wrap_spans}; use tuirealm::event::{Event, KeyCode}; -use tuirealm::props::{BordersProps, Props, PropsBuilder, Table as TextTable, TextParts}; +use tuirealm::props::{ + Alignment, BlockTitle, BordersProps, Props, PropsBuilder, Table as TextTable, +}; use tuirealm::tui::{ layout::{Corner, Rect}, style::{Color, Style}, widgets::{BorderType, Borders, List, ListItem, ListState}, }; -use tuirealm::{Canvas, Component, Msg, Payload, Value}; +use tuirealm::{Component, Frame, Msg, Payload, PropPayload, PropValue, Value}; // -- props +const PROP_TABLE: &str = "table"; + pub struct LogboxPropsBuilder { props: Option, } @@ -106,9 +110,18 @@ impl LogboxPropsBuilder { self } - pub fn with_log(&mut self, title: Option, table: TextTable) -> &mut Self { + pub fn with_title>(&mut self, text: S, alignment: Alignment) -> &mut Self { if let Some(props) = self.props.as_mut() { - props.texts = TextParts::table(title, table); + props.title = Some(BlockTitle::new(text, alignment)); + } + self + } + + pub fn with_log(&mut self, table: TextTable) -> &mut Self { + if let Some(props) = self.props.as_mut() { + props + .own + .insert(PROP_TABLE, PropPayload::One(PropValue::Table(table))); } self } @@ -198,33 +211,37 @@ impl LogBox { // Initialize states let mut states: OwnStates = OwnStates::default(); // Set list length - states.set_list_len(match &props.texts.table { - Some(rows) => rows.len(), - None => 0, - }); + states.set_list_len(Self::table_len(&props)); // Reset list index states.reset_list_index(); LogBox { props, states } } + + fn table_len(props: &Props) -> usize { + match props.own.get(PROP_TABLE) { + Some(PropPayload::One(PropValue::Table(table))) => table.len(), + _ => 0, + } + } } impl Component for LogBox { #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Canvas, area: Rect) { + fn render(&self, render: &mut Frame, area: Rect) { if self.props.visible { let width: usize = area.width as usize - 4; // Make list - let list_items: Vec = match self.props.texts.table.as_ref() { - None => Vec::new(), - Some(table) => table + let list_items: Vec = match self.props.own.get(PROP_TABLE) { + Some(PropPayload::One(PropValue::Table(table))) => table .iter() .map(|row| ListItem::new(wrap_spans(row, width, &self.props))) .collect(), // Make List item from TextSpan + _ => Vec::new(), }; let w = List::new(list_items) .block(get_block( &self.props.borders, - &self.props.texts.title, + self.props.title.as_ref(), self.states.focus, )) .start_corner(Corner::BottomLeft) @@ -240,10 +257,7 @@ impl Component for LogBox { fn update(&mut self, props: Props) -> Msg { self.props = props; // re-Set list length - self.states.set_list_len(match &self.props.texts.table { - Some(rows) => rows.len(), - None => 0, - }); + self.states.set_list_len(Self::table_len(&self.props)); // Reset list index self.states.reset_list_index(); Msg::None @@ -323,8 +337,8 @@ mod tests { .visible() .with_borders(Borders::ALL, BorderType::Double, Color::Red) .with_background(Color::Blue) + .with_title("Log", Alignment::Left) .with_log( - Some(String::from("Log")), TableBuilder::default() .add_col(TextSpan::from("12:29")) .add_col(TextSpan::from("system crashed")) @@ -337,11 +351,7 @@ mod tests { ); assert_eq!(component.props.visible, true); assert_eq!(component.props.background, Color::Blue); - assert_eq!( - component.props.texts.title.as_ref().unwrap().as_str(), - "Log" - ); - assert_eq!(component.props.texts.table.as_ref().unwrap().len(), 2); + assert_eq!(component.props.title.as_ref().unwrap().text(), "Log"); // Verify states assert_eq!(component.states.list_index, 0); assert_eq!(component.states.list_len, 2); @@ -364,7 +374,6 @@ mod tests { component.update( LogboxPropsBuilder::from(component.get_props()) .with_log( - Some(String::from("Log")), TableBuilder::default() .add_col(TextSpan::from("12:29")) .add_col(TextSpan::from("system crashed")) diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index a613ff9..bcb878a 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -30,4 +30,3 @@ pub mod bookmark_list; pub mod color_picker; pub mod file_list; pub mod logbox; -pub mod msgbox; diff --git a/src/ui/components/msgbox.rs b/src/ui/components/msgbox.rs deleted file mode 100644 index 226864a..0000000 --- a/src/ui/components/msgbox.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! ## MsgBox -//! -//! `MsgBox` component renders a simple readonly no event associated centered text - -/** - * MIT License - * - * termscp - Copyright (c) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// locals -use crate::utils::fmt::align_text_center; -// ext -use tuirealm::components::utils::{get_block, use_or_default_styles}; -use tuirealm::event::Event; -use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan}; -use tuirealm::tui::{ - layout::{Corner, Rect}, - style::{Color, Modifier, Style}, - text::{Span, Spans}, - widgets::{BorderType, Borders, List, ListItem}, -}; -use tuirealm::{Canvas, Component, Msg, Payload}; - -// -- Props - -pub struct MsgBoxPropsBuilder { - props: Option, -} - -impl Default for MsgBoxPropsBuilder { - fn default() -> Self { - MsgBoxPropsBuilder { - props: Some(Props::default()), - } - } -} - -impl PropsBuilder for MsgBoxPropsBuilder { - fn build(&mut self) -> Props { - self.props.take().unwrap() - } - - fn hidden(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.visible = false; - } - self - } - - fn visible(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.visible = true; - } - self - } -} - -impl From for MsgBoxPropsBuilder { - fn from(props: Props) -> Self { - MsgBoxPropsBuilder { props: Some(props) } - } -} - -impl MsgBoxPropsBuilder { - pub fn with_foreground(&mut self, color: Color) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.foreground = color; - } - self - } - - pub fn bold(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.modifiers |= Modifier::BOLD; - } - self - } - - pub fn blink(&mut self) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.modifiers |= Modifier::SLOW_BLINK; - } - self - } - - pub fn with_borders( - &mut self, - borders: Borders, - variant: BorderType, - color: Color, - ) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.borders = BordersProps { - borders, - variant, - color, - } - } - self - } - - pub fn with_texts(&mut self, title: Option, texts: Vec) -> &mut Self { - if let Some(props) = self.props.as_mut() { - props.texts = TextParts::new(title, Some(texts)); - } - self - } -} - -// -- component - -pub struct MsgBox { - props: Props, -} - -impl MsgBox { - /// ### new - /// - /// Instantiate a new Text component - pub fn new(props: Props) -> Self { - MsgBox { props } - } -} - -impl Component for MsgBox { - #[cfg(not(tarpaulin_include))] - fn render(&self, render: &mut Canvas, area: Rect) { - // Make a Span - if self.props.visible { - let lines: Vec = match self.props.texts.spans.as_ref() { - None => Vec::new(), - Some(rows) => { - let mut lines: Vec = Vec::new(); - for line in rows.iter() { - // Keep line color, or use default - let (fg, bg, modifiers) = use_or_default_styles(&self.props, line); - let message_row = - textwrap::wrap(line.content.as_str(), area.width as usize); - for msg in message_row.iter() { - lines.push(ListItem::new(Spans::from(vec![Span::styled( - align_text_center(msg, area.width), - Style::default().add_modifier(modifiers).fg(fg).bg(bg), - )]))); - } - } - lines - } - }; - render.render_widget( - List::new(lines) - .block(get_block( - &self.props.borders, - &self.props.texts.title, - true, - )) - .start_corner(Corner::TopLeft) - .style( - Style::default() - .fg(self.props.foreground) - .bg(self.props.background), - ), - area, - ); - } - } - - fn update(&mut self, props: Props) -> Msg { - self.props = props; - // Return None - Msg::None - } - - fn get_props(&self) -> Props { - self.props.clone() - } - - fn on(&mut self, ev: Event) -> Msg { - // Return key - if let Event::Key(key) = ev { - Msg::OnKey(key) - } else { - Msg::None - } - } - - fn get_state(&self) -> Payload { - Payload::None - } - - fn blur(&mut self) {} - - fn active(&mut self) {} -} - -#[cfg(test)] -mod tests { - - use super::*; - - use pretty_assertions::assert_eq; - use tuirealm::event::{KeyCode, KeyEvent}; - use tuirealm::props::{TextSpan, TextSpanBuilder}; - use tuirealm::tui::style::Color; - - #[test] - fn test_ui_components_msgbox() { - let mut component: MsgBox = MsgBox::new( - MsgBoxPropsBuilder::default() - .hidden() - .visible() - .with_foreground(Color::Red) - .bold() - .blink() - .with_borders(Borders::ALL, BorderType::Double, Color::Red) - .with_texts( - None, - vec![ - TextSpan::from("Press "), - TextSpanBuilder::new("") - .with_foreground(Color::Cyan) - .bold() - .build(), - TextSpan::from(" to quit"), - ], - ) - .build(), - ); - assert_eq!(component.props.foreground, Color::Red); - assert!(component.props.modifiers.intersects(Modifier::BOLD)); - assert_eq!(component.props.visible, true); - assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 3); - component.active(); - component.blur(); - // Update - let props = MsgBoxPropsBuilder::from(component.get_props()) - .hidden() - .with_foreground(Color::Yellow) - .build(); - assert_eq!(component.update(props), Msg::None); - assert_eq!(component.props.visible, false); - assert_eq!(component.props.foreground, Color::Yellow); - // Get value - assert_eq!(component.get_state(), Payload::None); - // Event - assert_eq!( - component.on(Event::Key(KeyEvent::from(KeyCode::Delete))), - Msg::OnKey(KeyEvent::from(KeyCode::Delete)) - ); - } -} diff --git a/src/utils/fmt.rs b/src/utils/fmt.rs index 703cd91..90e0194 100644 --- a/src/utils/fmt.rs +++ b/src/utils/fmt.rs @@ -25,6 +25,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +use crate::fs::UnixPex; + use chrono::prelude::*; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime}; @@ -32,55 +34,23 @@ use tuirealm::tui::style::Color; /// ### fmt_pex /// -/// Convert 3 bytes of permissions value into ls notation (e.g. rwx-wx--x) -pub fn fmt_pex(owner: u8, group: u8, others: u8) -> String { - let mut mode: String = String::with_capacity(9); - let read: u8 = (owner >> 2) & 0x1; - let write: u8 = (owner >> 1) & 0x1; - let exec: u8 = owner & 0x1; - mode.push_str(match read { - 1 => "r", - _ => "-", - }); - mode.push_str(match write { - 1 => "w", - _ => "-", - }); - mode.push_str(match exec { - 1 => "x", - _ => "-", - }); - let read: u8 = (group >> 2) & 0x1; - let write: u8 = (group >> 1) & 0x1; - let exec: u8 = group & 0x1; - mode.push_str(match read { - 1 => "r", - _ => "-", - }); - mode.push_str(match write { - 1 => "w", - _ => "-", - }); - mode.push_str(match exec { - 1 => "x", - _ => "-", - }); - let read: u8 = (others >> 2) & 0x1; - let write: u8 = (others >> 1) & 0x1; - let exec: u8 = others & 0x1; - mode.push_str(match read { - 1 => "r", - _ => "-", - }); - mode.push_str(match write { - 1 => "w", - _ => "-", - }); - mode.push_str(match exec { - 1 => "x", - _ => "-", - }); - mode +/// Convert permissions bytes of permissions value into ls notation (e.g. rwx,-wx,--x) +pub fn fmt_pex(pex: UnixPex) -> String { + format!( + "{}{}{}", + match pex.can_read() { + true => 'r', + false => '-', + }, + match pex.can_write() { + true => 'w', + false => '-', + }, + match pex.can_execute() { + true => 'x', + false => '-', + } + ) } /// ### instant_to_str @@ -100,23 +70,6 @@ pub fn fmt_millis(duration: Duration) -> String { format!("{}.{:0width$}", seconds, millis, width = 3) } -/// align_text_center -/// -/// Align text to center for a given width -pub fn align_text_center(text: &str, width: u16) -> String { - let indent_size: usize = match (width as usize) >= text.len() { - // NOTE: The check prevents underflow - true => (width as usize - text.len()) / 2, - false => 0, - }; - textwrap::indent( - text, - (0..indent_size).map(|_| " ").collect::().as_str(), - ) - .trim_end() - .to_string() -} - /// ### elide_path /// /// Elide a path if longer than width @@ -343,14 +296,9 @@ mod tests { #[test] fn test_utils_fmt_pex() { - assert_eq!(fmt_pex(7, 7, 7), String::from("rwxrwxrwx")); - assert_eq!(fmt_pex(7, 5, 5), String::from("rwxr-xr-x")); - assert_eq!(fmt_pex(6, 6, 6), String::from("rw-rw-rw-")); - assert_eq!(fmt_pex(6, 4, 4), String::from("rw-r--r--")); - assert_eq!(fmt_pex(6, 0, 0), String::from("rw-------")); - assert_eq!(fmt_pex(0, 0, 0), String::from("---------")); - assert_eq!(fmt_pex(4, 4, 4), String::from("r--r--r--")); - assert_eq!(fmt_pex(1, 2, 1), String::from("--x-w---x")); + assert_eq!(fmt_pex(UnixPex::from(7)), String::from("rwx")); + assert_eq!(fmt_pex(UnixPex::from(5)), String::from("r-x")); + assert_eq!(fmt_pex(UnixPex::from(6)), String::from("rw-")); } #[test] @@ -362,18 +310,6 @@ mod tests { ); } - #[test] - fn test_utils_align_text_center() { - assert_eq!( - align_text_center("hello world!", 24), - String::from(" hello world!") - ); - // Bad case - assert_eq!( - align_text_center("hello world!", 8), - String::from("hello world!") - ); - } #[test] fn test_utils_fmt_millis() { assert_eq!( diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f956857..71d2835 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -31,6 +31,7 @@ pub mod file; pub mod fmt; pub mod git; pub mod parser; +pub mod path; pub mod random; pub mod ui; diff --git a/src/utils/parser.rs b/src/utils/parser.rs index 1cb15c5..7359b44 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -202,6 +202,7 @@ pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result Result { match NaiveDateTime::parse_from_str(tm, fmt) { Ok(dt) => { diff --git a/src/utils/path.rs b/src/utils/path.rs new file mode 100644 index 0000000..1621009 --- /dev/null +++ b/src/utils/path.rs @@ -0,0 +1,43 @@ +//! # Path +//! +//! Path related utilities + +use std::path::{Path, PathBuf}; + +/// ### absolutize +/// +/// Absolutize target path if relative. +/// For example: +/// +/// ```rust +/// assert_eq!(absolutize(&Path::new("/home/omar"), &Path::new("readme.txt")).as_path(), Path::new("/home/omar/readme.txt")); +/// assert_eq!(absolutize(&Path::new("/home/omar"), &Path::new("/tmp/readme.txt")).as_path(), Path::new("/tmp/readme.txt")); +/// ``` +pub fn absolutize(wrkdir: &Path, target: &Path) -> PathBuf { + match target.is_absolute() { + true => target.to_path_buf(), + false => { + let mut p: PathBuf = wrkdir.to_path_buf(); + p.push(target); + p + } + } +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn absolutize_path() { + assert_eq!( + absolutize(&Path::new("/home/omar"), &Path::new("readme.txt")).as_path(), + Path::new("/home/omar/readme.txt") + ); + assert_eq!( + absolutize(&Path::new("/home/omar"), &Path::new("/tmp/readme.txt")).as_path(), + Path::new("/tmp/readme.txt") + ); + } +} diff --git a/src/utils/test_helpers.rs b/src/utils/test_helpers.rs index 45239c8..8ec02f0 100644 --- a/src/utils/test_helpers.rs +++ b/src/utils/test_helpers.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use crate::fs::{FsDirectory, FsEntry, FsFile}; +use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; // ext use std::fs::File; #[cfg(feature = "with-containers")] @@ -53,12 +53,11 @@ pub fn create_sample_file_entry() -> (FsFile, NamedTempFile) { last_access_time: SystemTime::UNIX_EPOCH, creation_time: SystemTime::UNIX_EPOCH, size: 127, - ftype: None, // File type - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + ftype: None, // File type + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }, tmpfile, ) @@ -162,11 +161,10 @@ pub fn make_fsentry(path: PathBuf, is_dir: bool) -> FsEntry { last_change_time: SystemTime::UNIX_EPOCH, last_access_time: SystemTime::UNIX_EPOCH, creation_time: SystemTime::UNIX_EPOCH, - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }), false => FsEntry::File(FsFile { name: path.file_name().unwrap().to_string_lossy().to_string(), @@ -175,12 +173,11 @@ pub fn make_fsentry(path: PathBuf, is_dir: bool) -> FsEntry { last_access_time: SystemTime::UNIX_EPOCH, creation_time: SystemTime::UNIX_EPOCH, size: 127, - ftype: None, // File type - readonly: false, - symlink: None, // UNIX only - user: Some(0), // UNIX only - group: Some(0), // UNIX only - unix_pex: Some((6, 4, 4)), // UNIX only + ftype: None, // File type + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only }), } } @@ -200,7 +197,7 @@ mod test { #[test] fn test_utils_test_helpers_sample_file() { let (file, _) = create_sample_file_entry(); - assert_eq!(file.readonly, false); + assert!(file.symlink.is_none()); } #[test]