Merge pull request #62 from veeso/0.6.1

0.6.1
This commit is contained in:
Christian Visintin 2021-08-30 15:00:06 +02:00 committed by GitHub
commit ddd4824567
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1617 additions and 2417 deletions

View file

@ -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

221
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -14,9 +14,9 @@
</p>
<p align="center">Developed by <a href="https://veeso.github.io/">@veeso</a></p>
<p align="center">Current version: 0.6.0 (23/07/2021)</p>
<p align="center">Current version: 0.6.1 (31/08/2021)</p>
[![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)

View file

@ -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: <<EOD

View file

@ -8,7 +8,7 @@
# -f, -y, --force, --yes
# Skip the confirmation prompt during installation
TERMSCP_VERSION="0.6.0"
TERMSCP_VERSION="0.6.1"
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
DEB_URL="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
FREEBSD_URL="${GITHUB_URL}/termscp-${TERMSCP_VERSION}.txz"

View file

@ -188,7 +188,7 @@ impl ActivityManager {
};
// If ft params is None, return None
let ft_params: &FileTransferParams = match ctx.ft_params() {
Some(ft_params) => &ft_params,
Some(ft_params) => ft_params,
None => {
error!("Failed to start FileTransferActivity: file transfer params is None");
return None;

View file

@ -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")
);

View file

@ -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<FsEntry, ()> {
// 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<FsEntry, ()> {
// Prepare list regex
// NOTE: about this damn regex <https://stackoverflow.com/questions/32480890/is-there-a-regex-to-parse-the-values-from-an-ftp-directory-listing>
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<usize>| {
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<u32> = match metadata.get(4).unwrap().as_str().parse::<u32>() {
Ok(uid) => Some(uid),
Err(_) => None,
};
// Get gid
let gid: Option<u32> = match metadata.get(5).unwrap().as_str().parse::<u32>() {
Ok(gid) => Some(gid),
Err(_) => None,
};
// Get filesize
let filesize: usize = metadata
.get(6)
.unwrap()
.as_str()
.parse::<usize>()
.unwrap_or(0);
// Split filename if required
let (file_name, symlink_path): (String, Option<PathBuf>) = 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<Box<FsEntry>> = 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<String> = 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<String>) -> Vec<FsEntry> {
// 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} {<DIR> | SIZE} {FILENAME}
/// 10-19-20 03:19PM <DIR> pub
/// 04-08-14 03:09PM 403 readme.txt
fn parse_dos_list_line(&self, path: &Path, line: &str) -> Result<FsEntry, ()> {
// 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+(<DIR>)?([\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::<usize>().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<String> = 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<Box<FsEntry>> {
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<PathBuf>) {
let tokens: Vec<&str> = token.split(" -> ").collect();
let filename: String = String::from(*tokens.get(0).unwrap());
let symlink: Option<PathBuf> = 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<FsEntry> = 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<Box<dyn Read>, 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<dyn Read>) -> 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 <DIR> 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());

View file

@ -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<String>,
@ -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

View file

@ -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<Box<FsEntry>> = 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<FsEntry, FileTransferError> {
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<Box<dyn Write>, 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());

View file

@ -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<u32> = metadata.uid;
let gid: Option<u32> = 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<FsEntry> = 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());

View file

@ -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);
}

View file

@ -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 {} {} {}",

View file

@ -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<Self, Self::Err> {
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
}),
}
}

View file

@ -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<Box<FsEntry>>, // UNIX only
pub user: Option<u32>, // UNIX only
pub group: Option<u32>, // UNIX only
pub unix_pex: Option<(u8, u8, u8)>, // UNIX only
pub symlink: Option<Box<FsEntry>>, // UNIX only
pub user: Option<u32>, // UNIX only
pub group: Option<u32>, // 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<String>, // File type
pub readonly: bool,
pub symlink: Option<Box<FsEntry>>, // UNIX only
pub user: Option<u32>, // UNIX only
pub group: Option<u32>, // UNIX only
pub unix_pex: Option<(u8, u8, u8)>, // UNIX only
pub ftype: Option<String>, // File type
pub symlink: Option<Box<FsEntry>>, // UNIX only
pub user: Option<u32>, // UNIX only
pub group: Option<u32>, // 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<u8> 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);
}
}

View file

@ -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<FsEntry, HostError> {
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)
}
}

View file

@ -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")]

View file

@ -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(),
)),
}

View file

@ -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(),
))
}

View file

@ -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(),
))
}

View file

@ -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();
}

View file

@ -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
// <RIGHT> / <LEFT>
(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
}
// <DEL | 'E'>
(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 {
}
}
// <ESC> 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,
// <TAB> 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 <TAB>, 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(),
// <ESC> => 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
}
}

View file

@ -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("<CTRL+H>")
.bold()
.with_foreground(key_color)
.build(),
TextSpanBuilder::new(" to show keybindings; ")
.bold()
.build(),
TextSpanBuilder::new("<CTRL+C>")
.bold()
.with_foreground(key_color)
.build(),
TextSpanBuilder::new(" to enter setup").bold().build(),
TextSpan::new("Press ").bold(),
TextSpan::new("<CTRL+H>").bold().fg(key_color),
TextSpan::new(" to show keybindings; ").bold(),
TextSpan::new("<CTRL+C>").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 <https://veeso.github.io/termscp/>; view release notes with <CTRL+R>"),
])
.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("<ESC>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<ESC>").bold().fg(key_color))
.add_col(TextSpan::from(" Quit termscp"))
.add_row()
.add_col(
TextSpanBuilder::new("<TAB>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<TAB>").bold().fg(key_color))
.add_col(TextSpan::from(" Switch from form and bookmarks"))
.add_row()
.add_col(
TextSpanBuilder::new("<RIGHT/LEFT>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(key_color))
.add_col(TextSpan::from(" Switch bookmark tab"))
.add_row()
.add_col(
TextSpanBuilder::new("<UP/DOWN>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(key_color))
.add_col(TextSpan::from(" Move up/down in current tab"))
.add_row()
.add_col(
TextSpanBuilder::new("<ENTER>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<ENTER>").bold().fg(key_color))
.add_col(TextSpan::from(" Connect/Load bookmark"))
.add_row()
.add_col(
TextSpanBuilder::new("<DEL|E>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<DEL|E>").bold().fg(key_color))
.add_col(TextSpan::from(" Delete selected bookmark"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+C>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color))
.add_col(TextSpan::from(" Enter setup"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+S>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<CTRL+S>").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(),
)),
);

View file

@ -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) => {

View file

@ -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,

View file

@ -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)

View file

@ -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)
}
}

View file

@ -228,7 +228,7 @@ impl FileTransferActivity {
///
/// Returns config client reference
fn config(&self) -> &ConfigClient {
&self.context().config()
self.context().config()
}
/// ### theme

View file

@ -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
),

View file

@ -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<String> = 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)
}
}

View file

@ -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<TextSpan> = 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<TextSpan> = 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("<ESC>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<ESC>").bold().fg(key_color))
.add_col(TextSpan::from(" Disconnect"))
.add_row()
.add_col(
TextSpanBuilder::new("<TAB>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<TAB>").bold().fg(key_color))
.add_col(TextSpan::from(
" Switch between explorer and logs",
))
.add_row()
.add_col(
TextSpanBuilder::new("<BACKSPACE>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<BACKSPACE>").bold().fg(key_color))
.add_col(TextSpan::from(" Go to previous directory"))
.add_row()
.add_col(
TextSpanBuilder::new("<RIGHT/LEFT>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(key_color))
.add_col(TextSpan::from(" Change explorer tab"))
.add_row()
.add_col(
TextSpanBuilder::new("<UP/DOWN>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(key_color))
.add_col(TextSpan::from(" Move up/down in list"))
.add_row()
.add_col(
TextSpanBuilder::new("<ENTER>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<ENTER>").bold().fg(key_color))
.add_col(TextSpan::from(" Enter directory"))
.add_row()
.add_col(
TextSpanBuilder::new("<SPACE>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<SPACE>").bold().fg(key_color))
.add_col(TextSpan::from(" Upload/Download file"))
.add_row()
.add_col(
TextSpanBuilder::new("<A>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<A>").bold().fg(key_color))
.add_col(TextSpan::from(" Toggle hidden files"))
.add_row()
.add_col(
TextSpanBuilder::new("<B>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<B>").bold().fg(key_color))
.add_col(TextSpan::from(" Change file sorting mode"))
.add_row()
.add_col(
TextSpanBuilder::new("<C>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<C>").bold().fg(key_color))
.add_col(TextSpan::from(" Copy"))
.add_row()
.add_col(
TextSpanBuilder::new("<D>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<D>").bold().fg(key_color))
.add_col(TextSpan::from(" Make directory"))
.add_row()
.add_col(
TextSpanBuilder::new("<G>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<G>").bold().fg(key_color))
.add_col(TextSpan::from(" Go to path"))
.add_row()
.add_col(
TextSpanBuilder::new("<H>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<H>").bold().fg(key_color))
.add_col(TextSpan::from(" Show help"))
.add_row()
.add_col(
TextSpanBuilder::new("<I>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<I>").bold().fg(key_color))
.add_col(TextSpan::from(" Show info about selected file"))
.add_row()
.add_col(
TextSpanBuilder::new("<L>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<L>").bold().fg(key_color))
.add_col(TextSpan::from(" Reload directory content"))
.add_row()
.add_col(
TextSpanBuilder::new("<M>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<M>").bold().fg(key_color))
.add_col(TextSpan::from(" Select file"))
.add_row()
.add_col(
TextSpanBuilder::new("<N>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<N>").bold().fg(key_color))
.add_col(TextSpan::from(" Create new file"))
.add_row()
.add_col(
TextSpanBuilder::new("<O>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<O>").bold().fg(key_color))
.add_col(TextSpan::from(
" Open text file with preferred editor",
))
.add_row()
.add_col(
TextSpanBuilder::new("<Q>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<Q>").bold().fg(key_color))
.add_col(TextSpan::from(" Quit termscp"))
.add_row()
.add_col(
TextSpanBuilder::new("<R>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<R>").bold().fg(key_color))
.add_col(TextSpan::from(" Rename file"))
.add_row()
.add_col(
TextSpanBuilder::new("<S>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<S>").bold().fg(key_color))
.add_col(TextSpan::from(" Save file as"))
.add_row()
.add_col(
TextSpanBuilder::new("<U>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<U>").bold().fg(key_color))
.add_col(TextSpan::from(" Go to parent directory"))
.add_row()
.add_col(
TextSpanBuilder::new("<V>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<V>").bold().fg(key_color))
.add_col(TextSpan::from(
" Open file with default application for file type",
))
.add_row()
.add_col(
TextSpanBuilder::new("<W>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<W>").bold().fg(key_color))
.add_col(TextSpan::from(
" Open file with specified application",
))
.add_row()
.add_col(
TextSpanBuilder::new("<X>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<X>").bold().fg(key_color))
.add_col(TextSpan::from(" Execute shell command"))
.add_row()
.add_col(
TextSpanBuilder::new("<Y>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<Y>").bold().fg(key_color))
.add_col(TextSpan::from(" Toggle synchronized browsing"))
.add_row()
.add_col(
TextSpanBuilder::new("<DEL|E>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<DEL|E>").bold().fg(key_color))
.add_col(TextSpan::from(" Delete selected file"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+A>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<CTRL+A>").bold().fg(key_color))
.add_col(TextSpan::from(" Select all files"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+C>")
.bold()
.with_foreground(key_color)
.build(),
)
.add_col(TextSpan::new("<CTRL+C>").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",
}
}

View file

@ -152,7 +152,7 @@ impl SetupActivity {
}
fn config(&self) -> &ConfigClient {
&self.context().config()
self.context().config()
}
fn config_mut(&mut self) -> &mut ConfigClient {

View file

@ -74,65 +74,67 @@ impl SetupActivity {
None => None,
Some(msg) => match msg {
// Input field <DOWN>
(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 <UP>
(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 <ENTER> or <ESC>
(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
}
// <CTRL+H> 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
}
// <CTRL+R> 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
}
// <CTRL+S> Save
(_, &MSG_KEY_CTRL_S) => {
(_, key) if key == &MSG_KEY_CTRL_S => {
// Show save
self.mount_save_popup();
None
}
// <ESC>
(_, &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 <ENTER> or <ESC>
(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
// <CTRL+H> Show help
(_, &MSG_KEY_CTRL_H) => {
(_, key) if key == &MSG_KEY_CTRL_H => {
// Show help
self.mount_help();
None
}
// New key <DOWN>
(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 <UP>
(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 <ESC>
(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
}
// <CTRL+N> 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
}
// <DEL | CTRL+E> 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
}
// <CTRL+R> 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
}
// <CTRL+S> Save
(_, &MSG_KEY_CTRL_S) => {
(_, key) if key == &MSG_KEY_CTRL_S => {
// Show save
self.mount_save_popup();
None
}
// <ESC>
(_, &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 <ENTER> or <ESC>
(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
// <CTRL+H> 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
}
// <CTRL+R> 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
}
// <CTRL+S> Save
(_, &MSG_KEY_CTRL_S) => {
(_, key) if key == &MSG_KEY_CTRL_S => {
// Show save
self.mount_save_popup();
None
}
// <ESC>
(_, &MSG_KEY_ESC) => {
(_, key) if key == &MSG_KEY_ESC => {
self.action_on_esc();
None
}

View file

@ -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("<CTRL+H>").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("<ESC>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::new("<ESC>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Exit setup"))
.add_row()
.add_col(
TextSpanBuilder::new("<TAB>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::new("<TAB>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change setup page"))
.add_row()
.add_col(
TextSpanBuilder::new("<RIGHT/LEFT>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change cursor"))
.add_row()
.add_col(
TextSpanBuilder::new("<UP/DOWN>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change input field"))
.add_row()
.add_col(
TextSpanBuilder::new("<ENTER>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::new("<ENTER>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Select / Dismiss popup"))
.add_row()
.add_col(
TextSpanBuilder::new("<DEL|E>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::new("<DEL|E>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Delete SSH key"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+N>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::new("<CTRL+N>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" New SSH key"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+R>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::new("<CTRL+R>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Revert changes"))
.add_row()
.add_col(
TextSpanBuilder::new("<CTRL+S>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Save configuration"))
.build(),
)

View file

@ -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("<CTRL+H>")
.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(),
)),
);

View file

@ -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("<CTRL+H>")
.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);
}

View file

@ -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("<CTRL+H>")
.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(),
)),
);

View file

@ -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<Props>,
@ -117,10 +118,19 @@ impl BookmarkListPropsBuilder {
self
}
pub fn with_bookmarks(&mut self, title: Option<String>, bookmarks: Vec<String>) -> &mut Self {
pub fn with_title<S: AsRef<str>>(&mut self, text: S, alignment: Alignment) -> &mut Self {
if let Some(props) = self.props.as_mut() {
let bookmarks: Vec<TextSpan> = 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<String>) -> &mut Self {
if let Some(props) = self.props.as_mut() {
let bookmarks: Vec<PropValue> = 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<ListItem> = match self.props.texts.spans.as_ref() {
None => vec![],
Some(lines) => lines
let list_item: Vec<ListItem> = 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

View file

@ -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<S: AsRef<str>>(&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

View file

@ -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<Props>,
@ -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<String>, files: Vec<String>) -> &mut Self {
pub fn with_title<S: AsRef<str>>(&mut self, text: S, alignment: Alignment) -> &mut Self {
if let Some(props) = self.props.as_mut() {
let files: Vec<TextSpan> = 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<String>) -> &mut Self {
if let Some(props) = self.props.as_mut() {
let files: Vec<PropValue> = 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<ListItem> = match self.props.texts.spans.as_ref() {
None => vec![],
Some(lines) => lines
let list_item: Vec<ListItem> = 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

View file

@ -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<Props>,
}
@ -106,9 +110,18 @@ impl LogboxPropsBuilder {
self
}
pub fn with_log(&mut self, title: Option<String>, table: TextTable) -> &mut Self {
pub fn with_title<S: AsRef<str>>(&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<ListItem> = match self.props.texts.table.as_ref() {
None => Vec::new(),
Some(table) => table
let list_items: Vec<ListItem> = 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"))

View file

@ -30,4 +30,3 @@ pub mod bookmark_list;
pub mod color_picker;
pub mod file_list;
pub mod logbox;
pub mod msgbox;

View file

@ -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<Props>,
}
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<Props> 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<String>, texts: Vec<TextSpan>) -> &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<ListItem> = match self.props.texts.spans.as_ref() {
None => Vec::new(),
Some(rows) => {
let mut lines: Vec<ListItem> = 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("<ESC>")
.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))
);
}
}

View file

@ -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::<String>().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!(

View file

@ -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;

View file

@ -202,6 +202,7 @@ pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result<SystemT
/// ### parse_datetime
///
/// Parse date time string representation and transform it into `SystemTime`
#[allow(dead_code)]
pub fn parse_datetime(tm: &str, fmt: &str) -> Result<SystemTime, ParseError> {
match NaiveDateTime::parse_from_str(tm, fmt) {
Ok(dt) => {

43
src/utils/path.rs Normal file
View file

@ -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")
);
}
}

View file

@ -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]