mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
23
.github/ISSUE_TEMPLATE/security.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/security.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Security report
|
||||
about: Create a report of a security vulnerability
|
||||
title: "[SECURITY] - ISSUE_TITLE"
|
||||
labels: security
|
||||
assignees: veeso
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Severity:
|
||||
|
||||
- [ ] **critical**
|
||||
- [ ] high
|
||||
- [ ] medium
|
||||
- [ ] low
|
||||
|
||||
A clear and concise description of the security vulnerability.
|
||||
|
||||
## Additional information
|
||||
|
||||
Add any other context about the problem here.
|
||||
19
.github/actions-rs/grcov.yml
vendored
19
.github/actions-rs/grcov.yml
vendored
@@ -3,12 +3,13 @@ ignore-not-existing: true
|
||||
llvm: true
|
||||
output-type: lcov
|
||||
ignore:
|
||||
- "/*"
|
||||
- "C:/*"
|
||||
- "../*"
|
||||
- src/main.rs
|
||||
- src/lib.rs
|
||||
- src/activity_manager.rs
|
||||
- "src/ui/activities/*"
|
||||
- src/ui/context.rs
|
||||
- src/ui/input.rs
|
||||
- "/*"
|
||||
- "C:/*"
|
||||
- "../*"
|
||||
- src/main.rs
|
||||
- src/lib.rs
|
||||
- src/activity_manager.rs
|
||||
- src/support.rs
|
||||
- "src/ui/activities/*"
|
||||
- src/ui/context.rs
|
||||
- src/ui/input.rs
|
||||
|
||||
21
.github/workflows/aur-pub.yml
vendored
21
.github/workflows/aur-pub.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: aur-pub
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
jobs:
|
||||
aur-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Publish AUR package
|
||||
uses: KSXGitHub/github-actions-deploy-aur@v2.2.4
|
||||
with:
|
||||
pkgname: termscp
|
||||
pkgbuild: ./dist/pkgs/arch/PKGBUILD
|
||||
commit_username: ${{ secrets.AUR_USERNAME }}
|
||||
commit_email: ${{ secrets.AUR_EMAIL }}
|
||||
ssh_private_key: ${{ secrets.AUR_KEY }}
|
||||
commit_message: Update AUR package
|
||||
ssh_keyscan_types: rsa,dsa,ecdsa,ed25519
|
||||
2
.github/workflows/coverage.yml
vendored
2
.github/workflows/coverage.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features --lib --no-fail-fast
|
||||
args: --lib --no-default-features --features github-actions --features with-containers --no-fail-fast
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
|
||||
|
||||
4
.github/workflows/freebsd.yml
vendored
4
.github/workflows/freebsd.yml
vendored
@@ -18,5 +18,5 @@ jobs:
|
||||
chmod +x /tmp/rustup.sh && \
|
||||
/tmp/rustup.sh -y
|
||||
. $HOME/.cargo/env
|
||||
cargo build
|
||||
cargo test --verbose --lib --features github-actions -- --test-threads 1
|
||||
cargo build --no-default-features
|
||||
cargo test --no-default-features --verbose --lib --features github-actions -- --test-threads 1
|
||||
|
||||
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features --lib --no-fail-fast
|
||||
args: --lib --no-default-features --features github-actions --features with-containers --no-fail-fast
|
||||
- name: Format
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Clippy
|
||||
|
||||
46
CHANGELOG.md
46
CHANGELOG.md
@@ -1,6 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
- [Changelog](#changelog)
|
||||
- [0.6.0](#060)
|
||||
- [0.5.1](#051)
|
||||
- [0.5.0](#050)
|
||||
- [0.4.2](#042)
|
||||
@@ -18,6 +19,51 @@
|
||||
|
||||
---
|
||||
|
||||
## 0.6.0
|
||||
|
||||
Released on 23/07/2021
|
||||
|
||||
> 🍹 Summer update 2021 🍨
|
||||
|
||||
- **Open any file** in explorer:
|
||||
- Open file with default program for file type with `<V>`
|
||||
- Open file with a specific program with `<W>`
|
||||
- **Themes**:
|
||||
- You can now set colors for 26 elements in the application
|
||||
- Colors can be any RGB, also **CSS colors** syntax is supported (e.g. `aquamarine`)
|
||||
- Configure theme from settings or import from CLI using the `-t <theme file>` argument
|
||||
- You can find several themes in the `themes/` directory
|
||||
- **Keyring support for Linux**
|
||||
- From now on keyring will be available for Linux only
|
||||
- Read the manual to find out if your system supports the keyring and how you can enable it
|
||||
- libdbus is now a dependency
|
||||
- added `with-keyring` feature
|
||||
- **❗ BREAKING CHANGE ❗**: if you start using keyring on Linux, all the saved password will be lost
|
||||
- **In-app release notes**
|
||||
- Possibility to see the release note of the new available release whenever a new version is available
|
||||
- Just press `<CTRL+R>` when a new version is available from the auth activity to read the release notes
|
||||
- **Installation script**:
|
||||
- From now on, in case cargo is used to install termscp, all the cargo dependencies will be installed
|
||||
- **Start termscp from configuration**: Start termscp with `-c` or `--config` to start termscp from configuration page
|
||||
- Enhancements:
|
||||
- Show a "wait" message when deleting, copying and moving files and when executing commands
|
||||
- Replaced all `...` with `…` in texts
|
||||
- Check if remote host is valid in authentication form
|
||||
- Check if port number is valid in authentication form
|
||||
- From now on, if you try to leave setup without making any change, you won't be prompted whether to save configuration or not
|
||||
- Bugfix:
|
||||
- Fixed broken input cursor when typing UTF8 characters (tui-realm 0.3.2)
|
||||
- Fixed save bookmark dialog: you could switch out from dialog with `<TAB>`
|
||||
- Fixed transfer interruption: it was not possible to abort a transfer if the size of the file was less than 65k
|
||||
- Changed `Remote address` to `Remote host` in authentication form
|
||||
- Dependencies:
|
||||
- Added `argh 0.1.5`
|
||||
- Added `open 1.7.0`
|
||||
- Removed `getopts`
|
||||
- Updated `rand` to `0.8.4`
|
||||
- Updated `textwrap` to `0.14.2`
|
||||
- Updated `tui-realm` to `0.4.3`
|
||||
|
||||
## 0.5.1
|
||||
|
||||
Released on 21/06/2021
|
||||
|
||||
166
Cargo.lock
generated
166
Cargo.lock
generated
@@ -1,5 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.6.0"
|
||||
@@ -49,6 +51,35 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argh"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e7317a549bc17c5278d9e72bb6e62c6aa801ac2567048e39ebc1c194249323e"
|
||||
dependencies = [
|
||||
"argh_derive",
|
||||
"argh_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argh_derive"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60949c42375351e9442e354434b0cba2ac402c1237edf673cac3a4bf983b8d3c"
|
||||
dependencies = [
|
||||
"argh_shared",
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argh_shared"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a61eb019cb8f415d162cb9f12130ee6bbe9168b7d953c17f4ad049e4051ca00"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
@@ -215,18 +246,18 @@ checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8"
|
||||
checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-any"
|
||||
version = "2.3.11"
|
||||
version = "2.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9950e91c5c444b0729f5f1b0aec76c523e01920ce828e37bccfa27803ff34e1"
|
||||
checksum = "79269446ee9793fb06fb297c61dd65e53e880fd10bdb222d544db1a98a2f083b"
|
||||
dependencies = [
|
||||
"debug-helper",
|
||||
]
|
||||
@@ -287,9 +318,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "debug-helper"
|
||||
version = "0.3.11"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4460596867846f73bddca51f7403b6a29f5315125be10a1640259b4db5b9494c"
|
||||
checksum = "76fbd10dce159c002b9c688ae8ab7cd531151e185e0ad360f4bfea3b0eede3a8"
|
||||
|
||||
[[package]]
|
||||
name = "des"
|
||||
@@ -400,15 +431,6 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
@@ -431,6 +453,15 @@ dependencies = [
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.10.0"
|
||||
@@ -517,9 +548,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.95"
|
||||
version = "0.2.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
|
||||
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
|
||||
|
||||
[[package]]
|
||||
name = "libssh2-sys"
|
||||
@@ -622,9 +653,9 @@ checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.7.11"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956"
|
||||
checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
@@ -747,9 +778,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.7.2"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
|
||||
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@@ -758,10 +789,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.34"
|
||||
name = "open"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d7830286ad6a3973c0f1d9b73738f69c76b739301d0229c4b96501695cbe4c8"
|
||||
checksum = "1711eb4b31ce4ad35b0f316d8dfba4fe5c7ad601c448446d84aae7a896627b20"
|
||||
dependencies = [
|
||||
"which",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -779,9 +820,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.63"
|
||||
version = "0.9.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6b0d6fb7d80f877617dfcb014e605e2b5ab2fb0afdf27935219bb6bd984cb98"
|
||||
checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
@@ -843,7 +884,7 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall 0.2.8",
|
||||
"redox_syscall 0.2.9",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
]
|
||||
@@ -917,14 +958,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
|
||||
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.0",
|
||||
"rand_core 0.6.2",
|
||||
"rand_hc 0.3.0",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.3",
|
||||
"rand_hc 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -939,12 +980,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.2",
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -958,9 +999,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
dependencies = [
|
||||
"getrandom 0.2.3",
|
||||
]
|
||||
@@ -976,11 +1017,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
|
||||
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
|
||||
dependencies = [
|
||||
"rand_core 0.6.2",
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -991,9 +1032,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
|
||||
checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
@@ -1005,7 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
|
||||
dependencies = [
|
||||
"getrandom 0.2.3",
|
||||
"redox_syscall 0.2.8",
|
||||
"redox_syscall 0.2.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1279,9 +1320,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.72"
|
||||
version = "1.0.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
|
||||
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1296,8 +1337,8 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"rand 0.8.3",
|
||||
"redox_syscall 0.2.8",
|
||||
"rand 0.8.4",
|
||||
"redox_syscall 0.2.9",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
]
|
||||
@@ -1313,8 +1354,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "termscp"
|
||||
version = "0.5.1"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"argh",
|
||||
"bitflags",
|
||||
"bytesize",
|
||||
"chrono",
|
||||
@@ -1323,15 +1365,15 @@ dependencies = [
|
||||
"dirs",
|
||||
"edit",
|
||||
"ftp4",
|
||||
"getopts",
|
||||
"hostname",
|
||||
"keyring",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"magic-crypt",
|
||||
"open",
|
||||
"path-slash",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.3",
|
||||
"rand 0.8.4",
|
||||
"regex",
|
||||
"rpassword",
|
||||
"serde",
|
||||
@@ -1350,9 +1392,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.14.0"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f59f5365546b8424b0cc48868ae4fbbbc29a538dcc496b53543525201034f0c2"
|
||||
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
@@ -1361,18 +1403,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.25"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6"
|
||||
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.25"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d"
|
||||
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1440,9 +1482,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tuirealm"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9897335542e4a4a87ad391419c35e54b4088661e671ba53e578fbbb1154740c2"
|
||||
checksum = "0fcbd06f2aa6a2424aaa245c10e8767fe3f0fee234ac8c144cb15eaf2ee37ce9"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"textwrap",
|
||||
@@ -1485,9 +1527,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.7.1"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
||||
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
@@ -1549,9 +1591,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.13"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "025ce40a007e1907e58d5bc1a594def78e5573bb0b1160bc389634e8f12e4faa"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
|
||||
18
Cargo.toml
18
Cargo.toml
@@ -11,7 +11,7 @@ license = "MIT"
|
||||
name = "termscp"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
version = "0.5.1"
|
||||
version = "0.6.0"
|
||||
|
||||
[package.metadata.rpm]
|
||||
package = "termscp"
|
||||
@@ -27,6 +27,7 @@ name = "termscp"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
argh = "0.1.5"
|
||||
bitflags = "1.2.1"
|
||||
bytesize = "1.0.1"
|
||||
chrono = "0.4.19"
|
||||
@@ -35,22 +36,23 @@ crossterm = "0.19.0"
|
||||
dirs = "3.0.1"
|
||||
edit = "0.1.3"
|
||||
ftp4 = { version = "4.0.2", features = [ "secure" ] }
|
||||
getopts = "0.2.21"
|
||||
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"
|
||||
rand = "0.8.3"
|
||||
open = "1.7.0"
|
||||
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"
|
||||
tempfile = "3.1.0"
|
||||
textwrap = "0.14.0"
|
||||
textwrap = "0.14.2"
|
||||
thiserror = "^1.0.0"
|
||||
toml = "0.5.8"
|
||||
tuirealm = { version = "0.4.2", features = [ "with-components" ] }
|
||||
tuirealm = { version = "0.4.3", features = [ "with-components" ] }
|
||||
ureq = { version = "2.1.0", features = [ "json" ] }
|
||||
whoami = "1.1.1"
|
||||
wildmatch = "2.0.0"
|
||||
@@ -59,17 +61,15 @@ wildmatch = "2.0.0"
|
||||
pretty_assertions = "0.7.2"
|
||||
|
||||
[features]
|
||||
default = [ "with-keyring" ]
|
||||
github-actions = []
|
||||
with-containers = []
|
||||
with-keyring = [ "keyring" ]
|
||||
|
||||
[target."cfg(target_family = \"unix\")"]
|
||||
[target."cfg(target_family = \"unix\")".dependencies]
|
||||
users = "0.11.0"
|
||||
|
||||
[target."cfg(any(target_os = \"windows\", target_os = \"macos\"))"]
|
||||
[target."cfg(any(target_os = \"windows\", target_os = \"macos\"))".dependencies]
|
||||
keyring = "0.10.1"
|
||||
|
||||
[target."cfg(target_os = \"windows\")"]
|
||||
[target."cfg(target_os = \"windows\")".dependencies]
|
||||
path-slash = "0.1.4"
|
||||
|
||||
48
README.md
48
README.md
@@ -14,7 +14,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Developed by <a href="https://veeso.github.io/">@veeso</a></p>
|
||||
<p align="center">Current version: 0.5.1 (21/06/2021)</p>
|
||||
<p align="center">Current version: 0.6.0 (23/07/2021)</p>
|
||||
|
||||
[](https://opensource.org/licenses/MIT) [](https://github.com/veeso/termscp) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
|
||||
@@ -32,7 +32,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
|
||||
|
||||
## Features 🎁
|
||||
|
||||
- 📁 Different communication protocols support
|
||||
- 📁 Different communication protocols
|
||||
- SFTP
|
||||
- SCP
|
||||
- FTP and FTPS
|
||||
@@ -43,6 +43,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
|
||||
- 💁 SFTP/SCP authentication through SSH keys and username/password
|
||||
- 🐧 Compatible with Windows, Linux, BSD and MacOS
|
||||
- ✏ Customizable
|
||||
- Themes
|
||||
- Custom file explorer format
|
||||
- Customizable text editor
|
||||
- Customizable file sorting
|
||||
@@ -69,6 +70,30 @@ while if you're a Windows user, you can install termscp with [Chocolatey](https:
|
||||
|
||||
For more information or other platforms, please visit [veeso.github.io](https://veeso.github.io/termscp/#get-started) to view all installation methods.
|
||||
|
||||
### Requirements ❗
|
||||
|
||||
- **Linux** users:
|
||||
- libssh
|
||||
- libdbus-1
|
||||
- **BSD** users:
|
||||
- libssh
|
||||
|
||||
### Optional Requirements ✔️
|
||||
|
||||
These requirements are not forcely required to run termscp, but to enjoy all of its features
|
||||
|
||||
- **Linux/BSD** users:
|
||||
- To **open** files via `V` (at least one of these)
|
||||
- *xdg-open*
|
||||
- *gio*
|
||||
- *gnome-open*
|
||||
- *kde-open*
|
||||
- **Linux** users:
|
||||
- A keyring manager: read more in the [User manual](docs/man.md#linux-keyring)
|
||||
- **WSL** users
|
||||
- To **open** files via `V` (at least one of these)
|
||||
- [wslu](https://github.com/wslutilities/wslu)
|
||||
|
||||
---
|
||||
|
||||
## Buy me a coffee ☕
|
||||
@@ -95,19 +120,17 @@ The developer documentation can be found on Rust Docs at <https://docs.rs/termsc
|
||||
|
||||
Major termscp releases will now be seasonal, so expect 4 major updates during the year.
|
||||
|
||||
Planned for *🏄 Summer update 🌴*:
|
||||
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.
|
||||
|
||||
Planned for *❄️ Winter update ⛄*:
|
||||
|
||||
- **Keyring-rs on Linux 🔐**: Check for updates in [this issue](https://github.com/veeso/termscp/issues/2)
|
||||
- **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 🦚~~
|
||||
- **Open files with any application ☄️**: possibility to open files of any kind and with any application directly inside termscp. This will be achieved through this awesome crate [open-rs](https://github.com/Byron/open-rs).
|
||||
|
||||
To be planned:
|
||||
|
||||
- **Themes provider 🎨**: I'm still thinking about how I will implement this, but basically the idea is to have a configuration file where it will be possible to define the color schema for the entire application. I haven't planned this release yet
|
||||
- **Configuration profile for bookmarks 📚**: I would like to, but I still have to analyze it.
|
||||
- **AWS S3 support 🪣**: There is already a library for AWS S3, but this is really on bottom of my implementation list at the moment, due to interest and I don't really have a system where to test it.
|
||||
|
||||
Along to new features, termscp developments is now focused on UI and performance improvements, so if you have any suggestion, feel free to open an issue.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -134,6 +157,7 @@ termscp is powered by these aweseome projects:
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm)
|
||||
- [edit](https://github.com/milkey-mouse/edit)
|
||||
- [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)
|
||||
|
||||
10
SECURITY.md
Normal file
10
SECURITY.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only latst version of termscp has the latest security updates.
|
||||
Because of that, **you should always consider updating termscp to the latest version**.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you have any security vulnerability or concern to report, please open an issue using the `Security report` template.
|
||||
BIN
assets/images/themes.gif
Normal file
BIN
assets/images/themes.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 328 KiB |
15
dist/build/deploy.sh
vendored
15
dist/build/deploy.sh
vendored
@@ -28,20 +28,5 @@ cd -
|
||||
mkdir -p ${PKGS_DIR}/rpm/
|
||||
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_centos7 termscp-${VERSION}-x86_64_centos7)
|
||||
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/release/rpmbuild/RPMS/x86_64/termscp-${VERSION}-1.el7.x86_64.rpm ${PKGS_DIR}/rpm/termscp-${VERSION}-1.x86_64.rpm
|
||||
# Build x86_64_archlinux
|
||||
|
||||
##################### TEMP REMOVED ###################################
|
||||
# cd x86_64_archlinux/
|
||||
# docker build --tag termscp-${VERSION}-x86_64_archlinux .
|
||||
# # Create container and get AUR pkg
|
||||
# cd -
|
||||
# mkdir -p ${PKGS_DIR}/arch/
|
||||
# CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_archlinux termscp-${VERSION}-x86_64_archlinux)
|
||||
# docker cp ${CONTAINER_NAME}:/usr/src/termscp/termscp-${VERSION}-x86_64.tar.gz ${PKGS_DIR}/arch/
|
||||
# docker cp ${CONTAINER_NAME}:/usr/src/termscp/PKGBUILD ${PKGS_DIR}/arch/
|
||||
# docker cp ${CONTAINER_NAME}:/usr/src/termscp/.SRCINFO ${PKGS_DIR}/arch/
|
||||
# # Replace termscp-bin with termscp in PKGBUILD
|
||||
# sed -i 's/termscp-bin/termscp/g' ${PKGS_DIR}/arch/PKGBUILD
|
||||
##################### TEMP REMOVED ###################################
|
||||
|
||||
exit $?
|
||||
|
||||
33
dist/build/x86_64_archlinux/Dockerfile
vendored
33
dist/build/x86_64_archlinux/Dockerfile
vendored
@@ -1,33 +0,0 @@
|
||||
FROM archlinux:latest as builder
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Install dependencies
|
||||
RUN pacman -Syu --noconfirm \
|
||||
git \
|
||||
gcc \
|
||||
openssl \
|
||||
pkg-config \
|
||||
sudo
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
chmod +x /tmp/rust.sh && \
|
||||
/tmp/rust.sh -y
|
||||
# Create build user
|
||||
RUN useradd build -m && \
|
||||
passwd -d build && \
|
||||
mkdir -p termscp && \
|
||||
chown -R build.build termscp/
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/veeso/termscp.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/termscp/
|
||||
# Install cargo arxch
|
||||
RUN source $HOME/.cargo/env && cargo install cargo-aur
|
||||
# Build for x86_64
|
||||
RUN source $HOME/.cargo/env && cargo build --release
|
||||
# Build pkgs
|
||||
RUN source $HOME/.cargo/env && cargo aur
|
||||
# Create SRCINFO
|
||||
RUN chown -R build.build ../termscp/ && sudo -u build bash -c 'makepkg --printsrcinfo > .SRCINFO'
|
||||
|
||||
CMD ["sh"]
|
||||
1
dist/build/x86_64_centos7/Dockerfile
vendored
1
dist/build/x86_64_centos7/Dockerfile
vendored
@@ -7,6 +7,7 @@ RUN yum -y install \
|
||||
gcc \
|
||||
openssl \
|
||||
pkgconfig \
|
||||
libdbus-devel \
|
||||
openssl-devel
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
|
||||
1
dist/build/x86_64_debian8/Dockerfile
vendored
1
dist/build/x86_64_debian8/Dockerfile
vendored
@@ -8,6 +8,7 @@ RUN apt update && apt install -y \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libssh2-1-dev \
|
||||
libdbus-1-dev \
|
||||
curl
|
||||
|
||||
# Install rust
|
||||
|
||||
1
dist/build/x86_64_debian9/Dockerfile
vendored
1
dist/build/x86_64_debian9/Dockerfile
vendored
@@ -8,6 +8,7 @@ RUN apt update && apt install -y \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libssh2-1-dev \
|
||||
libdbus-1-dev \
|
||||
curl
|
||||
|
||||
# Install rust
|
||||
|
||||
14
dist/pkgs/arch/.SRCINFO
vendored
14
dist/pkgs/arch/.SRCINFO
vendored
@@ -1,14 +0,0 @@
|
||||
pkgbase = termscp
|
||||
pkgdesc = termscp is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal.
|
||||
pkgver = 0.5.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/veeso/termscp
|
||||
arch = x86_64
|
||||
license = MIT
|
||||
provides = termscp
|
||||
options = strip
|
||||
source = https://github.com/veeso/termscp/releases/download/v0.5.1/termscp-0.5.1-x86_64.tar.gz
|
||||
sha256sums = f66a1d1602dc8ea336ba4a42bfbe818edc9c20722e1761b471b76109c272094c
|
||||
|
||||
pkgname = termscp
|
||||
|
||||
16
dist/pkgs/arch/PKGBUILD
vendored
16
dist/pkgs/arch/PKGBUILD
vendored
@@ -1,16 +0,0 @@
|
||||
# Maintainer: Christian Visintin
|
||||
pkgname=termscp
|
||||
pkgver=0.5.1
|
||||
pkgrel=1
|
||||
pkgdesc="termscp is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal."
|
||||
url="https://github.com/veeso/termscp"
|
||||
license=("MIT")
|
||||
arch=("x86_64")
|
||||
provides=("termscp")
|
||||
options=("strip")
|
||||
source=("https://github.com/veeso/termscp/releases/download/v$pkgver/termscp-$pkgver-x86_64.tar.gz")
|
||||
sha256sums=("f66a1d1602dc8ea336ba4a42bfbe818edc9c20722e1761b471b76109c272094c")
|
||||
|
||||
package() {
|
||||
install -Dm755 termscp -t "$pkgdir/usr/bin/"
|
||||
}
|
||||
0
dist/pkgs/freebsd/manifest
vendored
Executable file → Normal file
0
dist/pkgs/freebsd/manifest
vendored
Executable file → Normal file
@@ -1,35 +0,0 @@
|
||||
# Deploy checklist
|
||||
|
||||
Document audience: project maintainers
|
||||
|
||||
- [Deploy checklist](#deploy-checklist)
|
||||
- [Description](#description)
|
||||
- [Checklist](#checklist)
|
||||
|
||||
## Description
|
||||
|
||||
This document describes the checklist that must be fulfilled before releasing a new version of termscp.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] The latest build didn't report any error in the CI
|
||||
- [ ] All commands when using SFTP work
|
||||
- [ ] All commands when using SCP work
|
||||
- [ ] All commands when using FTP work
|
||||
- [ ] It is possible to load bookmarks
|
||||
- [ ] Recent connections get saved
|
||||
- [ ] Update versions and release date in readme, changelog and cargo.toml
|
||||
- [ ] Build on MacOS
|
||||
- [ ] Update sha256 and version on homebrew repository
|
||||
- [ ] Build on Windows
|
||||
- [ ] Update sha256 and version in chocolatey repository
|
||||
- [ ] Create chocolatey package
|
||||
- [ ] Build Linux version using docker from `dist/build/build.sh`
|
||||
- [ ] Update sha256 and version in AUR files
|
||||
- [ ] Create release and attach the following artifacts
|
||||
- [ ] Deb package
|
||||
- [ ] RPM package
|
||||
- [ ] MacOs tar.gz
|
||||
- [ ] Windows nupkg
|
||||
- [ ] Windows zip
|
||||
- [ ] AUR tar.gz
|
||||
149
docs/man.md
149
docs/man.md
@@ -1,5 +1,30 @@
|
||||
# User manual 🎓
|
||||
|
||||
- [User manual 🎓](#user-manual-)
|
||||
- [Usage ❓](#usage-)
|
||||
- [Address argument 🌎](#address-argument-)
|
||||
- [How Password can be provided 🔐](#how-password-can-be-provided-)
|
||||
- [File explorer 📂](#file-explorer-)
|
||||
- [Keybindings ⌨](#keybindings-)
|
||||
- [Work on multiple files 🥷](#work-on-multiple-files-)
|
||||
- [Synchronized browsing ⏲️](#synchronized-browsing-️)
|
||||
- [Open and Open With 🚪](#open-and-open-with-)
|
||||
- [Bookmarks ⭐](#bookmarks-)
|
||||
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
|
||||
- [Linux Keyring](#linux-keyring)
|
||||
- [KeepassXC setup for termscp](#keepassxc-setup-for-termscp)
|
||||
- [Configuration ⚙️](#configuration-️)
|
||||
- [SSH Key Storage 🔐](#ssh-key-storage-)
|
||||
- [File Explorer Format](#file-explorer-format)
|
||||
- [Themes 🎨](#themes-)
|
||||
- [Styles 💈](#styles-)
|
||||
- [Authentication page](#authentication-page)
|
||||
- [Transfer page](#transfer-page)
|
||||
- [Misc](#misc)
|
||||
- [Text Editor ✏](#text-editor-)
|
||||
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
|
||||
- [Logging 🩺](#logging-)
|
||||
|
||||
## Usage ❓
|
||||
|
||||
termscp can be started with the following options:
|
||||
@@ -7,7 +32,9 @@ termscp can be started with the following options:
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` if address is provided, password will be this argument
|
||||
- `-c, --config` Open termscp starting from the configuration page
|
||||
- `-q, --quiet` Disable logging
|
||||
- `-t, --theme <path>` Import specified theme
|
||||
- `-v, --version` Print version info
|
||||
- `-h, --help` Print help page
|
||||
|
||||
@@ -105,6 +132,8 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
|
||||
| `<R>` | Rename file | Rename |
|
||||
| `<S>` | Save file as... | Save |
|
||||
| `<U>` | Go to parent directory | Upper |
|
||||
| `<V>` | Open file with default program for filetype | View |
|
||||
| `<W>` | Open file with provided program | With |
|
||||
| `<X>` | Execute a command | eXecute |
|
||||
| `<Y>` | Toggle synchronized browsing | sYnc |
|
||||
| `<DEL>` | Delete file | |
|
||||
@@ -130,6 +159,23 @@ This means that whenever you'll change the working directory on one panel, the s
|
||||
|
||||
*Warning*: at the moment, whenever you try to access an unexisting directory, you won't be prompted to create it. This might change in a future update.
|
||||
|
||||
### Open and Open With 🚪
|
||||
|
||||
Open and open with commands are powered by [open-rs](https://docs.rs/crate/open/1.7.0).
|
||||
When opening files with View command (`<V>`), the system default application for the file type will be used. To do so, the default operting system service will be used, so be sure to have at least one of these installed on your system:
|
||||
|
||||
- **Windows** users: you don't have to worry about it, since the crate will use the `start` command.
|
||||
- **MacOS** users: you don't have to worry either, since the crate will use `open`, which is already installed on your system.
|
||||
- **Linux** users: one of these should be installed
|
||||
- *xdg-open*
|
||||
- *gio*
|
||||
- *gnome-open*
|
||||
- *kde-open*
|
||||
- **WSL** users: *wslview* is required, you must install [wslu](https://github.com/wslutilities/wslu).
|
||||
|
||||
> Q: Can I edit remote files using the view command?
|
||||
> A: No, at least not directly from the "remote panel". You have to download it to a local directory first, that's due to the fact that when you open a remote file, the file is downloaded into a temporary directory, but there's no way to create a watcher for the file to check when the program you used to open it was closed, so termscp is not able to know when you're done editing the file.
|
||||
|
||||
---
|
||||
|
||||
## Bookmarks ⭐
|
||||
@@ -168,13 +214,35 @@ whenever you want to use the previously saved connection, just press `<TAB>` to
|
||||
|
||||
### Are my passwords Safe 😈
|
||||
|
||||
Well, kinda.
|
||||
As said before, bookmarks are saved in your configuration directory along with passwords. Passwords are obviously not plain text, they are encrypted with **AES-128**. Does this make them safe? Well, depends on your operating system:
|
||||
Well, Yep 😉.
|
||||
As said before, bookmarks are saved in your configuration directory along with passwords. Passwords are obviously not plain text, they are encrypted with **AES-128**. Does this make them safe? Absolutely! (except for BSD and WSL users 😢)
|
||||
|
||||
On Windows and MacOS the passwords are stored, if possible (but should be), in respectively the Windows Vault and the Keychain. This is actually super-safe and is directly managed by your operating system.
|
||||
On **Windows**, **Linux** and **MacOS** the passwords are stored, if possible (but should be), respectively in the *Windows Vault*, in the *system keyring* and into the *Keychain*. This is actually super-safe and is directly managed by your operating system.
|
||||
|
||||
On Linux and BSD, on the other hand, the key used to encrypt your passwords is stored on your drive (at $HOME/.config/termscp). It is then, still possible to retrieve the key to decrypt passwords. Luckily, the location of the key guarantees your key can't be read by users different from yours, but yeah, I still wouldn't save the password for a server exposed on the internet 😉.
|
||||
Actually [keyring-rs](https://github.com/hwchen/keyring-rs), supports Linux, but for different reasons I preferred not to make it available for this configuration. If you want to read more about my decision read [this issue](https://github.com/veeso/termscp/issues/2), while if you think this might have been implemented differently feel free to open an issue with your proposal.
|
||||
❗ Please, notice that if you're a Linux user, you should really read the [chapter below 👀](#linux-keyring), because the keyring might not be enabled or supported on your system!
|
||||
|
||||
On *BSD* and *WSL*, on the other hand, the key used to encrypt your passwords is stored on your drive (at $HOME/.config/termscp). It is then, still possible to retrieve the key to decrypt passwords. Luckily, the location of the key guarantees your key can't be read by users different from yours, but yeah, I still wouldn't save the password for a server exposed on the internet 😉.
|
||||
|
||||
#### Linux Keyring
|
||||
|
||||
We all love Linux thanks to the freedom it gives to the users. You can basically do anything you want as a Linux user, but this has also some cons, such as the fact that often there is no standard applications across different distributions. And this involves keyring too.
|
||||
This means that on Linux there might be no keyring installed on your system. Unfortunately the library we use to work with the key storage requires a service which exposes `org.freedesktop.secrets` on D-BUS and the worst fact is that there only two services exposing it.
|
||||
|
||||
- ❗ If you use GNOME as desktop environment (e.g. ubuntu users), you should already be fine, since keyring is already provided by `gnome-keyring` and everything should already be working.
|
||||
- ❗ For other desktop environment users there is a nice program you can use to get a keyring which is [KeepassXC](https://keepassxc.org/), which I use on my Manjaro installation (with KDE) and works fine. The only problem is that you have to setup it to be used along with termscp (but it's quite simple). To get started with KeepassXC read more [here](#keepassxc-setup-for-termscp).
|
||||
- ❗ What about you don't want to install any of these services? Well, there's no problem! **termscp will keep working as usual**, but it will save the key in a file, as it usually does for BSD and WSL.
|
||||
|
||||
##### KeepassXC setup for termscp
|
||||
|
||||
Follow these steps in order to setup keepassXC for termscp:
|
||||
|
||||
1. Install KeepassXC
|
||||
2. Go to "tools" > "settings" in toolbar
|
||||
3. Select "Secret service integration" and toggle "Enable KeepassXC freedesktop.org secret service integration"
|
||||
4. Create a database, if you don't have one yet: from toolbar "Database" > "New database"
|
||||
5. From toolbar: "Database" > "Database settings"
|
||||
6. Select "Secret service integration" and toggle "Expose entries under this group"
|
||||
7. Select the group in the list where you want the termscp secret to be kept. Remember that this group might be used by any other application to store secrets via DBUS.
|
||||
|
||||
---
|
||||
|
||||
@@ -198,7 +266,8 @@ These parameters can be changed:
|
||||
- **Show Hidden Files**: select whether hidden files shall be displayed by default. You will be able to decide whether to show or not hidden files at runtime pressing `A` anyway.
|
||||
- **Check for updates**: if set to `yes`, termscp will fetch the Github API to check if there is a new version of termscp available.
|
||||
- **Group Dirs**: select whether directories should be groupped or not in file explorers. If `Display first` is selected, directories will be sorted using the configured method but displayed before files, viceversa if `Display last` is selected.
|
||||
- **File formatter syntax**: syntax to display file info for each file in the explorer. See [File explorer format](#file-explorer-format)
|
||||
- **Remote File formatter syntax**: syntax to display file info for each file in the remote explorer. See [File explorer format](#file-explorer-format)
|
||||
- **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format)
|
||||
|
||||
### SSH Key Storage 🔐
|
||||
|
||||
@@ -239,6 +308,74 @@ If left empty, the default formatter syntax will be used: `{NAME:24} {PEX} {USER
|
||||
|
||||
---
|
||||
|
||||
## Themes 🎨
|
||||
|
||||
Termscp provides you with an awesome feature: the possibility to set the colors for several components in the application.
|
||||
If you want to customize termscp there are two available ways to do so:
|
||||
|
||||
- From the **configuration menu**
|
||||
- Importing a **theme file**
|
||||
|
||||
In order to create your own customization from termscp, all you have to do so is to enter the configuration from the auth activity, pressing `<CTRL+C>` and then `<TAB>` twice. You should have now moved to the `themes` panel.
|
||||
|
||||
Here you can move with `<UP>` and `<DOWN>` to change the style you want to change, as shown in the gif below:
|
||||
|
||||

|
||||
|
||||
termscp supports both the traditional explicit hex (`#rrggbb`) and rgb `rgb(r, g, b)` syntax to provide colors, but also **[css colors](https://www.w3schools.com/cssref/css_colors.asp)** (such as `crimson`) are accepted 😉. There is also a special keywork which is `Default`. Default means that the color used will be the default foreground or background color based on the situation (foreground for texts and lines, background for well, guess what).
|
||||
|
||||
As said before, you can also import theme files. You can take inspiration from or directly use one of the themes provided along with termscp, located in the `themes/` directory of this repository and import them running termscp as `termscp -t <theme_file>`. If everything was fine, it should tell you the theme has successfully been imported.
|
||||
|
||||
### Styles 💈
|
||||
|
||||
You can find in the table below, the description for each style field.
|
||||
Please, notice that **styles won't apply to configuration page**, in order to make it always accessible in case you mess everything up
|
||||
|
||||
#### Authentication page
|
||||
|
||||
| Key | Description |
|
||||
|----------------|------------------------------------------|
|
||||
| auth_address | Color of the input field for IP address |
|
||||
| auth_bookmarks | Color of the bookmarks panel |
|
||||
| auth_password | Color of the input field for password |
|
||||
| auth_port | Color of the input field for port number |
|
||||
| auth_protocol | Color of the radio group for protocol |
|
||||
| auth_recents | Color of the recents panel |
|
||||
| auth_username | Color of the input field for username |
|
||||
|
||||
#### Transfer page
|
||||
|
||||
| Key | Description |
|
||||
|--------------------------------------|---------------------------------------------------------------------------|
|
||||
| transfer_local_explorer_background | Background color of localhost explorer |
|
||||
| transfer_local_explorer_foreground | Foreground coloor of localhost explorer |
|
||||
| transfer_local_explorer_highlighted | Border and highlighted color for localhost explorer |
|
||||
| transfer_remote_explorer_background | Background color of remote explorer |
|
||||
| transfer_remote_explorer_foreground | Foreground coloor of remote explorer |
|
||||
| transfer_remote_explorer_highlighted | Border and highlighted color for remote explorer |
|
||||
| transfer_log_background | Background color for log panel |
|
||||
| transfer_log_window | Window color for log panel |
|
||||
| transfer_progress_bar_partial | Partial progress bar color |
|
||||
| transfer_progress_bar_total | Total progress bar color |
|
||||
| transfer_status_hidden | Color for status bar "hidden" label |
|
||||
| transfer_status_sorting | Color for status bar "sorting" label; applies also to file sorting dialog |
|
||||
| transfer_status_sync_browsing | Color for status bar "sync browsing" label |
|
||||
|
||||
#### Misc
|
||||
|
||||
These styles applie to different part of the application.
|
||||
|
||||
| Key | Description |
|
||||
|-------------------|---------------------------------------------|
|
||||
| misc_error_dialog | Color for error messages |
|
||||
| misc_input_dialog | Color for input dialogs (such as copy file) |
|
||||
| misc_keys | Color of text for key strokes |
|
||||
| misc_quit_dialog | Color for quit dialogs |
|
||||
| misc_save_dialog | Color for save dialogs |
|
||||
| misc_warn_dialog | Color for warn dialogs |
|
||||
|
||||
---
|
||||
|
||||
## Text Editor ✏
|
||||
|
||||
termscp has, as you might have noticed, many features, one of these is the possibility to view and edit text file. It doesn't matter if the file is located on the local host or on the remote host, termscp provides the possibility to open a file in your favourite text editor.
|
||||
|
||||
115
install.sh
115
install.sh
@@ -8,7 +8,7 @@
|
||||
# -f, -y, --force, --yes
|
||||
# Skip the confirmation prompt during installation
|
||||
|
||||
TERMSCP_VERSION="0.5.1"
|
||||
TERMSCP_VERSION="0.6.0"
|
||||
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"
|
||||
@@ -81,6 +81,17 @@ download() {
|
||||
return $rc
|
||||
}
|
||||
|
||||
test_writeable() {
|
||||
local path
|
||||
path="${1:-}/test.txt"
|
||||
if touch "${path}" 2>/dev/null; then
|
||||
rm "${path}"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
elevate_priv() {
|
||||
if ! has sudo; then
|
||||
error 'Could not find the command "sudo", needed to install termscp on your system.'
|
||||
@@ -95,15 +106,16 @@ elevate_priv() {
|
||||
fi
|
||||
}
|
||||
|
||||
test_writeable() {
|
||||
local path
|
||||
path="${1:-}/test.txt"
|
||||
if touch "${path}" 2>/dev/null; then
|
||||
rm "${path}"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
elevate_priv_ex() {
|
||||
check_dir="$1"
|
||||
if test_writeable "$check_dir"; then
|
||||
sudo=""
|
||||
else
|
||||
warn "Root permissions are required to install dependecies"
|
||||
elevate_priv
|
||||
sudo="sudo"
|
||||
fi
|
||||
echo $sudo
|
||||
}
|
||||
|
||||
# Currently supporting:
|
||||
@@ -275,11 +287,92 @@ install_on_macos() {
|
||||
fi
|
||||
}
|
||||
|
||||
# -- cargo installation
|
||||
|
||||
install_bsd_cargo_deps() {
|
||||
set -e
|
||||
confirm "${YELLOW}libssh, gcc${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?"
|
||||
sudo="$(elevate_priv_ex /usr/local/bin)"
|
||||
$sudo pkg install -y curl wget libssh gcc
|
||||
info "Dependencies installed successfully"
|
||||
}
|
||||
|
||||
install_linux_cargo_deps() {
|
||||
local debian_deps="gcc pkg-config libssl-dev libssh2-1-dev libdbus-1-dev"
|
||||
local rpm_deps="gcc openssl pkgconfig libdbus-devel openssl-devel"
|
||||
local arch_deps="gcc openssl pkg-config dbus"
|
||||
local deps_cmd=""
|
||||
# Get pkg manager
|
||||
if has apt; then
|
||||
deps_cmd="apt install -y $debian_deps"
|
||||
elif has apt-get; then
|
||||
deps_cmd="apt-get install -y $debian_deps"
|
||||
elif has yum; then
|
||||
deps_cmd="yum -y install $rpm_deps"
|
||||
elif has dnf; then
|
||||
deps_cmd="dnf -y install $rpm_deps"
|
||||
elif has pacman; then
|
||||
deps_cmd="pacman -S --noconfirm $arch_deps"
|
||||
else
|
||||
error "Could not find any suitable package manager for your linux distro 🙄"
|
||||
error "Supported package manager are: 'apt', 'apt-get', 'yum', 'dnf', 'pacman'"
|
||||
exit 1
|
||||
fi
|
||||
set -e
|
||||
confirm "${YELLOW}libssh, gcc, openssl, pkg-config, libdbus${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}. The following command will be used to install the dependencies: '${BOLD}${YELLOW}${deps_cmd}${NO_COLOR}'. Would you like to proceed?"
|
||||
sudo="$(elevate_priv_ex /usr/local/bin)"
|
||||
$sudo $deps_cmd
|
||||
info "Dependencies installed successfully"
|
||||
}
|
||||
|
||||
install_cargo() {
|
||||
if has cargo; then
|
||||
return 0
|
||||
fi
|
||||
cargo_env="$HOME/.cargo/env"
|
||||
# Check if cargo is already installed (actually), but not loaded
|
||||
if [ -f $cargo_env ]; then
|
||||
. $cargo_env
|
||||
fi
|
||||
# Check again cargo
|
||||
if has cargo; then
|
||||
return 0
|
||||
else
|
||||
confirm "${YELLOW}rust${NO_COLOR} is required to build termscp with cargo; would you like to install it now?"
|
||||
set -e
|
||||
rustup=$(get_tmpfile "sh")
|
||||
info "Downloading rustup.sh…"
|
||||
download "${rustup}" "https://sh.rustup.rs"
|
||||
chmod +x $rustup
|
||||
$rustup -y
|
||||
info "Rust installed with success"
|
||||
. $cargo_env
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
try_with_cargo() {
|
||||
err="$1"
|
||||
# Install cargo
|
||||
install_cargo
|
||||
if has cargo; then
|
||||
info "Installing ${GREEN}termscp${NO_COLOR} via Cargo…"
|
||||
cargo install termscp
|
||||
case $PLATFORM in
|
||||
"freebsd")
|
||||
install_bsd_cargo_deps
|
||||
cargo install --no-default-features termscp
|
||||
;;
|
||||
|
||||
"linux")
|
||||
install_linux_cargo_deps
|
||||
cargo install termscp
|
||||
;;
|
||||
|
||||
*)
|
||||
cargo install termscp
|
||||
;;
|
||||
|
||||
esac
|
||||
else
|
||||
error "$err"
|
||||
error "Alternatively you can opt for installing Cargo <https://www.rust-lang.org/tools/install>"
|
||||
|
||||
@@ -26,15 +26,16 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::host::{HostError, Localhost};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::environment;
|
||||
use crate::system::theme_provider::ThemeProvider;
|
||||
use crate::ui::activities::{
|
||||
auth::AuthActivity, filetransfer::FileTransferActivity, setup::SetupActivity, Activity,
|
||||
ExitReason,
|
||||
};
|
||||
use crate::ui::context::{Context, FileTransferParams};
|
||||
use crate::ui::context::Context;
|
||||
|
||||
// Namespaces
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -66,15 +67,16 @@ impl ActivityManager {
|
||||
pub fn new(local_dir: &Path, interval: Duration) -> Result<ActivityManager, HostError> {
|
||||
// Prepare Context
|
||||
// Initialize configuration client
|
||||
let (config_client, error): (Option<ConfigClient>, Option<String>) =
|
||||
let (config_client, error): (ConfigClient, Option<String>) =
|
||||
match Self::init_config_client() {
|
||||
Ok(cli) => (Some(cli), None),
|
||||
Ok(cli) => (cli, None),
|
||||
Err(err) => {
|
||||
error!("Failed to initialize config client: {}", err);
|
||||
(None, Some(err))
|
||||
(ConfigClient::degraded(), Some(err))
|
||||
}
|
||||
};
|
||||
let ctx: Context = Context::new(config_client, error);
|
||||
let theme_provider: ThemeProvider = Self::init_theme_provider();
|
||||
let ctx: Context = Context::new(config_client, theme_provider, error);
|
||||
Ok(ActivityManager {
|
||||
context: Some(ctx),
|
||||
local_dir: local_dir.to_path_buf(),
|
||||
@@ -85,24 +87,9 @@ impl ActivityManager {
|
||||
/// ### set_filetransfer_params
|
||||
///
|
||||
/// Set file transfer params
|
||||
pub fn set_filetransfer_params(
|
||||
&mut self,
|
||||
address: String,
|
||||
port: u16,
|
||||
protocol: FileTransferProtocol,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
entry_directory: Option<PathBuf>,
|
||||
) {
|
||||
pub fn set_filetransfer_params(&mut self, params: FileTransferParams) {
|
||||
// Put params into the context
|
||||
self.context.as_mut().unwrap().ft_params = Some(FileTransferParams {
|
||||
address,
|
||||
port,
|
||||
protocol,
|
||||
username,
|
||||
password,
|
||||
entry_directory,
|
||||
});
|
||||
self.context.as_mut().unwrap().set_ftparams(params);
|
||||
}
|
||||
|
||||
/// ### run
|
||||
@@ -200,7 +187,7 @@ impl ActivityManager {
|
||||
}
|
||||
};
|
||||
// If ft params is None, return None
|
||||
let ft_params: &FileTransferParams = match ctx.ft_params.as_ref() {
|
||||
let ft_params: &FileTransferParams = match ctx.ft_params() {
|
||||
Some(ft_params) => &ft_params,
|
||||
None => {
|
||||
error!("Failed to start FileTransferActivity: file transfer params is None");
|
||||
@@ -306,7 +293,7 @@ impl ActivityManager {
|
||||
}
|
||||
}
|
||||
None => Err(String::from(
|
||||
"Your system doesn't support configuration paths",
|
||||
"Your system doesn't provide a configuration directory",
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -316,4 +303,32 @@ impl ActivityManager {
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_theme_provider() -> ThemeProvider {
|
||||
match environment::init_config_dir() {
|
||||
Ok(config_dir) => {
|
||||
match config_dir {
|
||||
Some(config_dir) => {
|
||||
// Get config client paths
|
||||
let theme_path: PathBuf = environment::get_theme_path(config_dir.as_path());
|
||||
match ThemeProvider::new(theme_path.as_path()) {
|
||||
Ok(provider) => provider,
|
||||
Err(err) => {
|
||||
error!("Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode", theme_path.display(), err);
|
||||
ThemeProvider::degraded()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("This system doesn't provide a configuration directory; using theme provider in degraded mode");
|
||||
ThemeProvider::degraded()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Could not initialize configuration directory: {}; using theme provider in degraded mode", err);
|
||||
ThemeProvider::degraded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
//! ## Serializer
|
||||
//!
|
||||
//! `serializer` is the module which provides the serializer/deserializer for bookmarks
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
use super::{SerializerError, SerializerErrorKind, UserHosts};
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub struct BookmarkSerializer;
|
||||
|
||||
impl BookmarkSerializer {
|
||||
/// ### serialize
|
||||
///
|
||||
/// Serialize `UserHosts` into TOML and write content to writable
|
||||
pub fn serialize(
|
||||
&self,
|
||||
mut writable: Box<dyn Write>,
|
||||
hosts: &UserHosts,
|
||||
) -> Result<(), SerializerError> {
|
||||
// Serialize content
|
||||
let data: String = match toml::ser::to_string(hosts) {
|
||||
Ok(dt) => dt,
|
||||
Err(err) => {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SerializationError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
trace!("Serialized new bookmarks data: {}", data);
|
||||
// Write file
|
||||
match writable.write_all(data.as_bytes()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### deserialize
|
||||
///
|
||||
/// Read data from readable and deserialize its content as TOML
|
||||
pub fn deserialize(&self, mut readable: Box<dyn Read>) -> Result<UserHosts, SerializerError> {
|
||||
// Read file content
|
||||
let mut data: String = String::new();
|
||||
if let Err(err) = readable.read_to_string(&mut data) {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
trace!("Read bookmarks from file: {}", data);
|
||||
// Deserialize
|
||||
match toml::de::from_str(data.as_str()) {
|
||||
Ok(bookmarks) => {
|
||||
debug!("Read bookmarks from file {:?}", bookmarks);
|
||||
Ok(bookmarks)
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::super::Bookmark;
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Seek, SeekFrom};
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_serializer_deserialize_ok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
let hosts = deserializer.deserialize(Box::new(toml_file));
|
||||
assert!(hosts.is_ok());
|
||||
let hosts: UserHosts = hosts.ok().unwrap();
|
||||
// Verify hosts
|
||||
// Verify recents
|
||||
assert_eq!(hosts.recents.len(), 1);
|
||||
let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap();
|
||||
assert_eq!(host.address, String::from("172.16.104.10"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SCP"));
|
||||
assert_eq!(host.username, String::from("root"));
|
||||
assert_eq!(host.password, None);
|
||||
// Verify bookmarks
|
||||
assert_eq!(hosts.bookmarks.len(), 3);
|
||||
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
|
||||
assert_eq!(host.address, String::from("192.168.1.31"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SFTP"));
|
||||
assert_eq!(host.username, String::from("root"));
|
||||
assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword"));
|
||||
let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap();
|
||||
assert_eq!(host.address, String::from("192.168.1.30"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SFTP"));
|
||||
assert_eq!(host.username, String::from("cvisintin"));
|
||||
assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret"));
|
||||
let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap();
|
||||
assert_eq!(host.address, String::from("51.23.67.12"));
|
||||
assert_eq!(host.port, 21);
|
||||
assert_eq!(host.protocol, String::from("FTPS"));
|
||||
assert_eq!(host.username, String::from("aws001"));
|
||||
assert_eq!(host.password, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_serializer_deserialize_nok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_bad_toml();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
assert!(deserializer.deserialize(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_serializer_serialize() {
|
||||
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(2);
|
||||
// Push two samples
|
||||
bookmarks.insert(
|
||||
String::from("raspberrypi2"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.31"),
|
||||
port: 22,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("root"),
|
||||
password: None,
|
||||
},
|
||||
);
|
||||
bookmarks.insert(
|
||||
String::from("msi-estrem"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.30"),
|
||||
port: 4022,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("cvisintin"),
|
||||
password: Some(String::from("password")),
|
||||
},
|
||||
);
|
||||
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
|
||||
recents.insert(
|
||||
String::from("ISO20201215T094000Z"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.254"),
|
||||
port: 3022,
|
||||
protocol: String::from("SCP"),
|
||||
username: String::from("omar"),
|
||||
password: Some(String::from("aaa")),
|
||||
},
|
||||
);
|
||||
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
// Serialize
|
||||
let deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
let hosts: UserHosts = UserHosts { bookmarks, recents };
|
||||
assert!(deserializer.serialize(Box::new(tmpfile), &hosts).is_ok());
|
||||
}
|
||||
|
||||
fn create_good_toml() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" }
|
||||
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" }
|
||||
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
|
||||
|
||||
[recents]
|
||||
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
//write!(tmpfile, "[bookmarks]\nraspberrypi2 = {{ address = \"192.168.1.31\", port = 22, protocol = \"SFTP\", username = \"root\" }}\nmsi-estrem = {{ address = \"192.168.1.30\", port = 22, protocol = \"SFTP\", username = \"cvisintin\" }}\naws-server-prod1 = {{ address = \"51.23.67.12\", port = 21, protocol = \"FTPS\", username = \"aws001\" }}\n\n[recents]\nISO20201215T094000Z = {{ address = \"172.16.104.10\", port = 22, protocol = \"SCP\", username = \"root\" }}\n");
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_bad_toml() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root"}
|
||||
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP" }
|
||||
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
|
||||
|
||||
[recents]
|
||||
ISO20201215T094000Z = { address = "172.16.104.10", protocol = "SCP", username = "root", port = 22 }
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,8 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
pub mod serializer;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## UserHosts
|
||||
@@ -53,66 +50,15 @@ pub struct Bookmark {
|
||||
pub password: Option<String>, // Password is optional; base64, aes-128 encrypted password
|
||||
}
|
||||
|
||||
// Errors
|
||||
|
||||
/// ## SerializerError
|
||||
///
|
||||
/// Contains the error for serializer/deserializer
|
||||
#[derive(std::fmt::Debug)]
|
||||
pub struct SerializerError {
|
||||
kind: SerializerErrorKind,
|
||||
msg: Option<String>,
|
||||
}
|
||||
|
||||
/// ## SerializerErrorKind
|
||||
///
|
||||
/// Describes the kind of error for the serializer/deserializer
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SerializerErrorKind {
|
||||
#[error("IO error")]
|
||||
IoError,
|
||||
#[error("Serialization error")]
|
||||
SerializationError,
|
||||
#[error("Syntax error")]
|
||||
SyntaxError,
|
||||
}
|
||||
|
||||
impl Default for UserHosts {
|
||||
fn default() -> Self {
|
||||
UserHosts {
|
||||
Self {
|
||||
bookmarks: HashMap::new(),
|
||||
recents: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializerError {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new `SerializerError`
|
||||
pub fn new(kind: SerializerErrorKind) -> SerializerError {
|
||||
SerializerError { kind, msg: None }
|
||||
}
|
||||
|
||||
/// ### new_ex
|
||||
///
|
||||
/// Instantiates a new `SerializerError` with description message
|
||||
pub fn new_ex(kind: SerializerErrorKind, msg: String) -> SerializerError {
|
||||
let mut err: SerializerError = SerializerError::new(kind);
|
||||
err.msg = Some(msg);
|
||||
err
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SerializerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match &self.msg {
|
||||
Some(msg) => write!(f, "{} ({})", self.kind, msg),
|
||||
None => write!(f, "{}", self.kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -121,6 +67,13 @@ mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_default() {
|
||||
let bookmarks: UserHosts = UserHosts::default();
|
||||
assert_eq!(bookmarks.bookmarks.len(), 0);
|
||||
assert_eq!(bookmarks.recents.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_bookmark_new() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
@@ -168,30 +121,4 @@ mod tests {
|
||||
String::from("password")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_bookmark_errors() {
|
||||
let error: SerializerError = SerializerError::new(SerializerErrorKind::SyntaxError);
|
||||
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"));
|
||||
assert!(error.msg.is_some());
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
String::from("Syntax error (bad syntax)")
|
||||
);
|
||||
// Fmt
|
||||
assert_eq!(
|
||||
format!("{}", SerializerError::new(SerializerErrorKind::IoError)),
|
||||
String::from("IO error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
SerializerError::new(SerializerErrorKind::SerializationError)
|
||||
),
|
||||
String::from("Serialization error")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## Config
|
||||
//!
|
||||
//! `config` is the module which provides access to termscp configuration
|
||||
//! `config` is the module which provides access to all the termscp configurations
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
@@ -25,240 +25,10 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Modules
|
||||
pub mod serializer;
|
||||
// export
|
||||
pub use params::*;
|
||||
|
||||
// Deps
|
||||
extern crate edit;
|
||||
|
||||
// Locals
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
|
||||
// Ext
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## UserConfig
|
||||
///
|
||||
/// UserConfig contains all the configurations for the user,
|
||||
/// supported by termscp
|
||||
pub struct UserConfig {
|
||||
pub user_interface: UserInterfaceConfig,
|
||||
pub remote: RemoteConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## UserInterfaceConfig
|
||||
///
|
||||
/// UserInterfaceConfig provides all the keys to configure the user interface
|
||||
pub struct UserInterfaceConfig {
|
||||
pub text_editor: PathBuf,
|
||||
pub default_protocol: String,
|
||||
pub show_hidden_files: bool,
|
||||
pub check_for_updates: Option<bool>, // @! Since 0.3.3
|
||||
pub group_dirs: Option<String>,
|
||||
pub file_fmt: Option<String>, // Refers to local host (for backward compatibility)
|
||||
pub remote_file_fmt: Option<String>, // @! Since 0.5.0
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## RemoteConfig
|
||||
///
|
||||
/// Contains configuratio related to remote hosts
|
||||
pub struct RemoteConfig {
|
||||
pub ssh_keys: HashMap<String, PathBuf>, // Association between host name and path to private key
|
||||
}
|
||||
|
||||
impl Default for UserConfig {
|
||||
fn default() -> Self {
|
||||
UserConfig {
|
||||
user_interface: UserInterfaceConfig::default(),
|
||||
remote: RemoteConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UserInterfaceConfig {
|
||||
fn default() -> Self {
|
||||
UserInterfaceConfig {
|
||||
text_editor: match edit::get_editor() {
|
||||
Ok(p) => p,
|
||||
Err(_) => PathBuf::from("nano"), // Default to nano
|
||||
},
|
||||
default_protocol: FileTransferProtocol::Sftp.to_string(),
|
||||
show_hidden_files: false,
|
||||
check_for_updates: Some(true),
|
||||
group_dirs: None,
|
||||
file_fmt: None,
|
||||
remote_file_fmt: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RemoteConfig {
|
||||
fn default() -> Self {
|
||||
RemoteConfig {
|
||||
ssh_keys: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Errors
|
||||
|
||||
/// ## SerializerError
|
||||
///
|
||||
/// Contains the error for serializer/deserializer
|
||||
#[derive(std::fmt::Debug)]
|
||||
pub struct SerializerError {
|
||||
kind: SerializerErrorKind,
|
||||
msg: Option<String>,
|
||||
}
|
||||
|
||||
/// ## SerializerErrorKind
|
||||
///
|
||||
/// Describes the kind of error for the serializer/deserializer
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SerializerErrorKind {
|
||||
#[error("IO error")]
|
||||
IoError,
|
||||
#[error("Serialization error")]
|
||||
SerializationError,
|
||||
#[error("Syntax error")]
|
||||
SyntaxError,
|
||||
}
|
||||
|
||||
impl SerializerError {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new `SerializerError`
|
||||
pub fn new(kind: SerializerErrorKind) -> SerializerError {
|
||||
SerializerError { kind, msg: None }
|
||||
}
|
||||
|
||||
/// ### new_ex
|
||||
///
|
||||
/// Instantiates a new `SerializerError` with description message
|
||||
pub fn new_ex(kind: SerializerErrorKind, msg: String) -> SerializerError {
|
||||
let mut err: SerializerError = SerializerError::new(kind);
|
||||
err.msg = Some(msg);
|
||||
err
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SerializerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match &self.msg {
|
||||
Some(msg) => write!(f, "{} ({})", self.kind, msg),
|
||||
None => write!(f, "{}", self.kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_config_mod_new() {
|
||||
let mut keys: HashMap<String, PathBuf> = HashMap::with_capacity(1);
|
||||
keys.insert(
|
||||
String::from("192.168.1.31"),
|
||||
PathBuf::from("/tmp/private.key"),
|
||||
);
|
||||
let remote: RemoteConfig = RemoteConfig { ssh_keys: keys };
|
||||
let ui: UserInterfaceConfig = UserInterfaceConfig {
|
||||
default_protocol: String::from("SFTP"),
|
||||
text_editor: PathBuf::from("nano"),
|
||||
show_hidden_files: true,
|
||||
check_for_updates: Some(true),
|
||||
group_dirs: Some(String::from("first")),
|
||||
file_fmt: Some(String::from("{NAME}")),
|
||||
remote_file_fmt: Some(String::from("{USER}")),
|
||||
};
|
||||
assert_eq!(ui.default_protocol, String::from("SFTP"));
|
||||
assert_eq!(ui.text_editor, PathBuf::from("nano"));
|
||||
assert_eq!(ui.show_hidden_files, true);
|
||||
assert_eq!(ui.check_for_updates, Some(true));
|
||||
assert_eq!(ui.group_dirs, Some(String::from("first")));
|
||||
assert_eq!(ui.file_fmt, Some(String::from("{NAME}")));
|
||||
let cfg: UserConfig = UserConfig {
|
||||
user_interface: ui,
|
||||
remote: remote,
|
||||
};
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.31"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/tmp/private.key")
|
||||
);
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.check_for_updates, Some(true));
|
||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
|
||||
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
|
||||
assert_eq!(
|
||||
cfg.user_interface.remote_file_fmt,
|
||||
Some(String::from("{USER}"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_mod_new_default() {
|
||||
// Force vim editor
|
||||
env::set_var(String::from("EDITOR"), String::from("vim"));
|
||||
// Get default
|
||||
let cfg: UserConfig = UserConfig::default();
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
|
||||
// Text editor
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
PathBuf::from(cfg.user_interface.text_editor.file_name().unwrap()), // NOTE: since edit 0.1.3 real path is used
|
||||
PathBuf::from("vim.EXE")
|
||||
);
|
||||
#[cfg(target_family = "unix")]
|
||||
assert_eq!(
|
||||
PathBuf::from(cfg.user_interface.text_editor.file_name().unwrap()), // NOTE: since edit 0.1.3 real path is used
|
||||
PathBuf::from("vim")
|
||||
);
|
||||
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
|
||||
assert_eq!(cfg.remote.ssh_keys.len(), 0);
|
||||
assert!(cfg.user_interface.file_fmt.is_none());
|
||||
assert!(cfg.user_interface.remote_file_fmt.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_mod_errors() {
|
||||
let error: SerializerError = SerializerError::new(SerializerErrorKind::SyntaxError);
|
||||
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"));
|
||||
assert!(error.msg.is_some());
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
String::from("Syntax error (bad syntax)")
|
||||
);
|
||||
// Fmt
|
||||
assert_eq!(
|
||||
format!("{}", SerializerError::new(SerializerErrorKind::IoError)),
|
||||
String::from("IO error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
SerializerError::new(SerializerErrorKind::SerializationError)
|
||||
),
|
||||
String::from("Serialization error")
|
||||
);
|
||||
}
|
||||
}
|
||||
pub mod bookmarks;
|
||||
pub mod params;
|
||||
pub mod serialization;
|
||||
pub mod themes;
|
||||
|
||||
155
src/config/params.rs
Normal file
155
src/config/params.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
//! ## Config
|
||||
//!
|
||||
//! `config` is the module which provides access to termscp configuration
|
||||
|
||||
/**
|
||||
* 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::filetransfer::FileTransferProtocol;
|
||||
|
||||
// Ext
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## UserConfig
|
||||
///
|
||||
/// UserConfig contains all the configurations for the user,
|
||||
/// supported by termscp
|
||||
pub struct UserConfig {
|
||||
pub user_interface: UserInterfaceConfig,
|
||||
pub remote: RemoteConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## UserInterfaceConfig
|
||||
///
|
||||
/// UserInterfaceConfig provides all the keys to configure the user interface
|
||||
pub struct UserInterfaceConfig {
|
||||
pub text_editor: PathBuf,
|
||||
pub default_protocol: String,
|
||||
pub show_hidden_files: bool,
|
||||
pub check_for_updates: Option<bool>, // @! Since 0.3.3
|
||||
pub group_dirs: Option<String>,
|
||||
pub file_fmt: Option<String>, // Refers to local host (for backward compatibility)
|
||||
pub remote_file_fmt: Option<String>, // @! Since 0.5.0
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## RemoteConfig
|
||||
///
|
||||
/// Contains configuratio related to remote hosts
|
||||
pub struct RemoteConfig {
|
||||
pub ssh_keys: HashMap<String, PathBuf>, // Association between host name and path to private key
|
||||
}
|
||||
|
||||
impl Default for UserConfig {
|
||||
fn default() -> Self {
|
||||
UserConfig {
|
||||
user_interface: UserInterfaceConfig::default(),
|
||||
remote: RemoteConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UserInterfaceConfig {
|
||||
fn default() -> Self {
|
||||
UserInterfaceConfig {
|
||||
text_editor: match edit::get_editor() {
|
||||
Ok(p) => p,
|
||||
Err(_) => PathBuf::from("nano"), // Default to nano
|
||||
},
|
||||
default_protocol: FileTransferProtocol::Sftp.to_string(),
|
||||
show_hidden_files: false,
|
||||
check_for_updates: Some(true),
|
||||
group_dirs: None,
|
||||
file_fmt: None,
|
||||
remote_file_fmt: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RemoteConfig {
|
||||
fn default() -> Self {
|
||||
RemoteConfig {
|
||||
ssh_keys: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_config_mod_new() {
|
||||
let mut keys: HashMap<String, PathBuf> = HashMap::with_capacity(1);
|
||||
keys.insert(
|
||||
String::from("192.168.1.31"),
|
||||
PathBuf::from("/tmp/private.key"),
|
||||
);
|
||||
let remote: RemoteConfig = RemoteConfig { ssh_keys: keys };
|
||||
let ui: UserInterfaceConfig = UserInterfaceConfig {
|
||||
default_protocol: String::from("SFTP"),
|
||||
text_editor: PathBuf::from("nano"),
|
||||
show_hidden_files: true,
|
||||
check_for_updates: Some(true),
|
||||
group_dirs: Some(String::from("first")),
|
||||
file_fmt: Some(String::from("{NAME}")),
|
||||
remote_file_fmt: Some(String::from("{USER}")),
|
||||
};
|
||||
assert_eq!(ui.default_protocol, String::from("SFTP"));
|
||||
assert_eq!(ui.text_editor, PathBuf::from("nano"));
|
||||
assert_eq!(ui.show_hidden_files, true);
|
||||
assert_eq!(ui.check_for_updates, Some(true));
|
||||
assert_eq!(ui.group_dirs, Some(String::from("first")));
|
||||
assert_eq!(ui.file_fmt, Some(String::from("{NAME}")));
|
||||
let cfg: UserConfig = UserConfig {
|
||||
user_interface: ui,
|
||||
remote: remote,
|
||||
};
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.31"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/tmp/private.key")
|
||||
);
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.check_for_updates, Some(true));
|
||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
|
||||
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
|
||||
assert_eq!(
|
||||
cfg.user_interface.remote_file_fmt,
|
||||
Some(String::from("{USER}"))
|
||||
);
|
||||
}
|
||||
}
|
||||
575
src/config/serialization.rs
Normal file
575
src/config/serialization.rs
Normal file
@@ -0,0 +1,575 @@
|
||||
//! ## Serialization
|
||||
//!
|
||||
//! `serialization` provides serialization and deserialization for configurations
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::io::{Read, Write};
|
||||
use thiserror::Error;
|
||||
|
||||
/// ## SerializerError
|
||||
///
|
||||
/// Contains the error for serializer/deserializer
|
||||
#[derive(std::fmt::Debug)]
|
||||
pub struct SerializerError {
|
||||
kind: SerializerErrorKind,
|
||||
msg: Option<String>,
|
||||
}
|
||||
|
||||
/// ## SerializerErrorKind
|
||||
///
|
||||
/// Describes the kind of error for the serializer/deserializer
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SerializerErrorKind {
|
||||
#[error("Operation failed")]
|
||||
GenericError,
|
||||
#[error("IO error")]
|
||||
IoError,
|
||||
#[error("Serialization error")]
|
||||
SerializationError,
|
||||
#[error("Syntax error")]
|
||||
SyntaxError,
|
||||
}
|
||||
|
||||
impl SerializerError {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new `SerializerError`
|
||||
pub fn new(kind: SerializerErrorKind) -> SerializerError {
|
||||
SerializerError { kind, msg: None }
|
||||
}
|
||||
|
||||
/// ### new_ex
|
||||
///
|
||||
/// Instantiates a new `SerializerError` with description message
|
||||
pub fn new_ex(kind: SerializerErrorKind, msg: String) -> SerializerError {
|
||||
let mut err: SerializerError = SerializerError::new(kind);
|
||||
err.msg = Some(msg);
|
||||
err
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SerializerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match &self.msg {
|
||||
Some(msg) => write!(f, "{} ({})", self.kind, msg),
|
||||
None => write!(f, "{}", self.kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### serialize
|
||||
///
|
||||
/// Serialize `UserHosts` into TOML and write content to writable
|
||||
pub fn serialize<S>(serializable: &S, mut writable: Box<dyn Write>) -> Result<(), SerializerError>
|
||||
where
|
||||
S: Serialize + Sized,
|
||||
{
|
||||
// Serialize content
|
||||
let data: String = match toml::ser::to_string(serializable) {
|
||||
Ok(dt) => dt,
|
||||
Err(err) => {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SerializationError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
trace!("Serialized new bookmarks data: {}", data);
|
||||
// Write file
|
||||
match writable.write_all(data.as_bytes()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### deserialize
|
||||
///
|
||||
/// Read data from readable and deserialize its content as TOML
|
||||
pub fn deserialize<S>(mut readable: Box<dyn Read>) -> Result<S, SerializerError>
|
||||
where
|
||||
S: DeserializeOwned + Sized + std::fmt::Debug,
|
||||
{
|
||||
// Read file content
|
||||
let mut data: String = String::new();
|
||||
if let Err(err) = readable.read_to_string(&mut data) {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
trace!("Read bookmarks from file: {}", data);
|
||||
// Deserialize
|
||||
match toml::de::from_str(data.as_str()) {
|
||||
Ok(deserialized) => {
|
||||
debug!("Read bookmarks from file {:?}", deserialized);
|
||||
Ok(deserialized)
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use std::path::PathBuf;
|
||||
use tuirealm::tui::style::Color;
|
||||
|
||||
use crate::config::bookmarks::{Bookmark, UserHosts};
|
||||
use crate::config::params::UserConfig;
|
||||
use crate::config::themes::Theme;
|
||||
use crate::utils::test_helpers::create_file_ioers;
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization_errors() {
|
||||
let error: SerializerError = SerializerError::new(SerializerErrorKind::SyntaxError);
|
||||
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"));
|
||||
assert!(error.msg.is_some());
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
String::from("Syntax error (bad syntax)")
|
||||
);
|
||||
// Fmt
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
SerializerError::new(SerializerErrorKind::GenericError)
|
||||
),
|
||||
String::from("Operation failed")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", SerializerError::new(SerializerErrorKind::IoError)),
|
||||
String::from("IO error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
SerializerError::new(SerializerErrorKind::SerializationError)
|
||||
),
|
||||
String::from("Serialization error")
|
||||
);
|
||||
}
|
||||
|
||||
// -- Serialization of params
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization_params_deserialize_ok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml_bookmarks_params();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let cfg = deserialize(Box::new(toml_file));
|
||||
assert!(cfg.is_ok());
|
||||
let cfg: UserConfig = cfg.ok().unwrap();
|
||||
// Verify configuration
|
||||
// Verify ui
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
|
||||
assert_eq!(
|
||||
cfg.user_interface.file_fmt,
|
||||
Some(String::from("{NAME} {PEX}"))
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.user_interface.remote_file_fmt,
|
||||
Some(String::from("{NAME} {USER}")),
|
||||
);
|
||||
// Verify keys
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.31"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/raspberry.key")
|
||||
);
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.32"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/beaglebone.key")
|
||||
);
|
||||
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization_params_deserialize_ok_no_opts() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml_bookmarks_params_no_opts();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let cfg = deserialize(Box::new(toml_file));
|
||||
assert!(cfg.is_ok());
|
||||
let cfg: UserConfig = cfg.ok().unwrap();
|
||||
// Verify configuration
|
||||
// Verify ui
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, None);
|
||||
assert!(cfg.user_interface.check_for_updates.is_none());
|
||||
assert!(cfg.user_interface.file_fmt.is_none());
|
||||
assert!(cfg.user_interface.remote_file_fmt.is_none());
|
||||
// Verify keys
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.31"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/raspberry.key")
|
||||
);
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.32"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/beaglebone.key")
|
||||
);
|
||||
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization_params_deserialize_nok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_bad_toml_bookmarks_params();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
assert!(deserialize::<UserConfig>(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization_params_serialize() {
|
||||
let mut cfg: UserConfig = UserConfig::default();
|
||||
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
|
||||
// Insert key
|
||||
cfg.remote.ssh_keys.insert(
|
||||
String::from("192.168.1.31"),
|
||||
PathBuf::from("/home/omar/.ssh/id_rsa"),
|
||||
);
|
||||
// Serialize
|
||||
let writer: Box<dyn Write> = Box::new(std::fs::File::create(toml_file.path()).unwrap());
|
||||
assert!(serialize(&cfg, writer).is_ok());
|
||||
// Reload configuration and check if it's ok
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
assert!(deserialize::<UserConfig>(Box::new(toml_file)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization_params_fail_write() {
|
||||
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
|
||||
let writer: Box<dyn Write> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
|
||||
// Try to write unexisting file
|
||||
let cfg: UserConfig = UserConfig::default();
|
||||
assert!(serialize(&cfg, writer).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization_params_fail_read() {
|
||||
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
|
||||
let reader: Box<dyn Read> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
|
||||
// Try to write unexisting file
|
||||
assert!(deserialize::<UserConfig>(reader).is_err());
|
||||
}
|
||||
|
||||
fn create_good_toml_bookmarks_params() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SCP"
|
||||
text_editor = "vim"
|
||||
show_hidden_files = true
|
||||
check_for_updates = true
|
||||
group_dirs = "last"
|
||||
file_fmt = "{NAME} {PEX}"
|
||||
remote_file_fmt = "{NAME} {USER}"
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_good_toml_bookmarks_params_no_opts() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SCP"
|
||||
text_editor = "vim"
|
||||
show_hidden_files = true
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_bad_toml_bookmarks_params() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SFTP"
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
// -- bookmarks
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_bookmarks_serializer_deserialize_ok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml_bookmarks();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let hosts = deserialize(Box::new(toml_file));
|
||||
assert!(hosts.is_ok());
|
||||
let hosts: UserHosts = hosts.ok().unwrap();
|
||||
// Verify hosts
|
||||
// Verify recents
|
||||
assert_eq!(hosts.recents.len(), 1);
|
||||
let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap();
|
||||
assert_eq!(host.address, String::from("172.16.104.10"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SCP"));
|
||||
assert_eq!(host.username, String::from("root"));
|
||||
assert_eq!(host.password, None);
|
||||
// Verify bookmarks
|
||||
assert_eq!(hosts.bookmarks.len(), 3);
|
||||
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
|
||||
assert_eq!(host.address, String::from("192.168.1.31"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SFTP"));
|
||||
assert_eq!(host.username, String::from("root"));
|
||||
assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword"));
|
||||
let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap();
|
||||
assert_eq!(host.address, String::from("192.168.1.30"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SFTP"));
|
||||
assert_eq!(host.username, String::from("cvisintin"));
|
||||
assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret"));
|
||||
let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap();
|
||||
assert_eq!(host.address, String::from("51.23.67.12"));
|
||||
assert_eq!(host.port, 21);
|
||||
assert_eq!(host.protocol, String::from("FTPS"));
|
||||
assert_eq!(host.username, String::from("aws001"));
|
||||
assert_eq!(host.password, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_bookmarks_serializer_deserialize_nok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_bad_toml_bookmarks();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
assert!(deserialize::<UserHosts>(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_bookmarks_serializer_serialize() {
|
||||
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(2);
|
||||
// Push two samples
|
||||
bookmarks.insert(
|
||||
String::from("raspberrypi2"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.31"),
|
||||
port: 22,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("root"),
|
||||
password: None,
|
||||
},
|
||||
);
|
||||
bookmarks.insert(
|
||||
String::from("msi-estrem"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.30"),
|
||||
port: 4022,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("cvisintin"),
|
||||
password: Some(String::from("password")),
|
||||
},
|
||||
);
|
||||
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
|
||||
recents.insert(
|
||||
String::from("ISO20201215T094000Z"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.254"),
|
||||
port: 3022,
|
||||
protocol: String::from("SCP"),
|
||||
username: String::from("omar"),
|
||||
password: Some(String::from("aaa")),
|
||||
},
|
||||
);
|
||||
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
// Serialize
|
||||
let hosts: UserHosts = UserHosts { bookmarks, recents };
|
||||
assert!(serialize(&hosts, Box::new(tmpfile)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization_theme_serialize() {
|
||||
let mut theme: Theme = Theme::default();
|
||||
theme.auth_address = Color::Rgb(240, 240, 240);
|
||||
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let (reader, writer) = create_file_ioers(tmpfile.path());
|
||||
assert!(serialize(&theme, Box::new(writer)).is_ok());
|
||||
// Try to deserialize
|
||||
let deserialized_theme: Theme = deserialize(Box::new(reader)).ok().unwrap();
|
||||
assert_eq!(theme, deserialized_theme);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization_theme_deserialize() {
|
||||
let toml_file = create_good_toml_theme();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok());
|
||||
let toml_file = create_bad_toml_theme();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
fn create_good_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" }
|
||||
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" }
|
||||
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
|
||||
|
||||
[recents]
|
||||
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
//write!(tmpfile, "[bookmarks]\nraspberrypi2 = {{ address = \"192.168.1.31\", port = 22, protocol = \"SFTP\", username = \"root\" }}\nmsi-estrem = {{ address = \"192.168.1.30\", port = 22, protocol = \"SFTP\", username = \"cvisintin\" }}\naws-server-prod1 = {{ address = \"51.23.67.12\", port = 21, protocol = \"FTPS\", username = \"aws001\" }}\n\n[recents]\nISO20201215T094000Z = {{ address = \"172.16.104.10\", port = 22, protocol = \"SCP\", username = \"root\" }}\n");
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_bad_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root"}
|
||||
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP" }
|
||||
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
|
||||
|
||||
[recents]
|
||||
ISO20201215T094000Z = { address = "172.16.104.10", protocol = "SCP", username = "root", port = 22 }
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_good_toml_theme() -> tempfile::NamedTempFile {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r##"auth_address = "Yellow"
|
||||
auth_bookmarks = "LightGreen"
|
||||
auth_password = "LightBlue"
|
||||
auth_port = "LightCyan"
|
||||
auth_protocol = "LightGreen"
|
||||
auth_recents = "LightBlue"
|
||||
auth_username = "LightMagenta"
|
||||
misc_error_dialog = "Red"
|
||||
misc_input_dialog = "240,240,240"
|
||||
misc_keys = "Cyan"
|
||||
misc_quit_dialog = "Yellow"
|
||||
misc_save_dialog = "Cyan"
|
||||
misc_warn_dialog = "LightRed"
|
||||
transfer_local_explorer_background = "rgb(240, 240, 240)"
|
||||
transfer_local_explorer_foreground = "rgb(60, 60, 60)"
|
||||
transfer_local_explorer_highlighted = "Yellow"
|
||||
transfer_log_background = "255, 255, 255"
|
||||
transfer_log_window = "LightGreen"
|
||||
transfer_progress_bar_full = "forestgreen"
|
||||
transfer_progress_bar_partial = "Green"
|
||||
transfer_remote_explorer_background = "#f0f0f0"
|
||||
transfer_remote_explorer_foreground = "rgb(40, 40, 40)"
|
||||
transfer_remote_explorer_highlighted = "LightBlue"
|
||||
transfer_status_hidden = "LightBlue"
|
||||
transfer_status_sorting = "LightYellow"
|
||||
transfer_status_sync_browsing = "LightGreen"
|
||||
"##;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_bad_toml_theme() -> tempfile::NamedTempFile {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
auth_address = "Yellow"
|
||||
auth_bookmarks = "LightGreen"
|
||||
auth_password = "LightBlue"
|
||||
auth_port = "LightCyan"
|
||||
auth_protocol = "LightGreen"
|
||||
auth_recents = "LightBlue"
|
||||
auth_username = "LightMagenta"
|
||||
misc_error_dialog = "Red"
|
||||
misc_input_dialog = "240,240,240"
|
||||
misc_keys = "Cyan"
|
||||
misc_quit_dialog = "Yellow"
|
||||
misc_warn_dialog = "LightRed"
|
||||
transfer_local_explorer_text = "rgb(240, 240, 240)"
|
||||
transfer_local_explorer_window = "Yellow"
|
||||
transfer_log_text = "255, 255, 255"
|
||||
transfer_log_window = "LightGreen"
|
||||
transfer_progress_bar = "Green"
|
||||
transfer_remote_explorer_text = "verdazzurro"
|
||||
transfer_remote_explorer_window = "LightBlue"
|
||||
transfer_status_hidden = "LightBlue"
|
||||
transfer_status_sorting = "LightYellow"
|
||||
transfer_status_sync_browsing = "LightGreen"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
//! ## Serializer
|
||||
//!
|
||||
//! `serializer` is the module which provides the serializer/deserializer for configuration
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
use super::{SerializerError, SerializerErrorKind, UserConfig};
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub struct ConfigSerializer;
|
||||
|
||||
impl ConfigSerializer {
|
||||
/// ### serialize
|
||||
///
|
||||
/// Serialize `UserConfig` into TOML and write content to writable
|
||||
pub fn serialize(
|
||||
&self,
|
||||
mut writable: Box<dyn Write>,
|
||||
cfg: &UserConfig,
|
||||
) -> Result<(), SerializerError> {
|
||||
// Serialize content
|
||||
let data: String = match toml::ser::to_string(cfg) {
|
||||
Ok(dt) => dt,
|
||||
Err(err) => {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SerializationError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
trace!("Serialized new configuration data: {}", data);
|
||||
// Write file
|
||||
match writable.write_all(data.as_bytes()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### deserialize
|
||||
///
|
||||
/// Read data from readable and deserialize its content as TOML
|
||||
pub fn deserialize(&self, mut readable: Box<dyn Read>) -> Result<UserConfig, SerializerError> {
|
||||
// Read file content
|
||||
let mut data: String = String::new();
|
||||
if let Err(err) = readable.read_to_string(&mut data) {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
trace!("Read configuration from file: {}", data);
|
||||
// Deserialize
|
||||
match toml::de::from_str(data.as_str()) {
|
||||
Ok(config) => {
|
||||
debug!("Read config from file {:?}", config);
|
||||
Ok(config)
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_deserialize_ok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
let cfg = deserializer.deserialize(Box::new(toml_file));
|
||||
assert!(cfg.is_ok());
|
||||
let cfg: UserConfig = cfg.ok().unwrap();
|
||||
// Verify configuration
|
||||
// Verify ui
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
|
||||
assert_eq!(
|
||||
cfg.user_interface.file_fmt,
|
||||
Some(String::from("{NAME} {PEX}"))
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.user_interface.remote_file_fmt,
|
||||
Some(String::from("{NAME} {USER}")),
|
||||
);
|
||||
// Verify keys
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.31"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/raspberry.key")
|
||||
);
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.32"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/beaglebone.key")
|
||||
);
|
||||
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_deserialize_ok_no_opts() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml_no_opts();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
let cfg = deserializer.deserialize(Box::new(toml_file));
|
||||
assert!(cfg.is_ok());
|
||||
let cfg: UserConfig = cfg.ok().unwrap();
|
||||
// Verify configuration
|
||||
// Verify ui
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, None);
|
||||
assert!(cfg.user_interface.check_for_updates.is_none());
|
||||
assert!(cfg.user_interface.file_fmt.is_none());
|
||||
assert!(cfg.user_interface.remote_file_fmt.is_none());
|
||||
// Verify keys
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.31"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/raspberry.key")
|
||||
);
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.32"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/beaglebone.key")
|
||||
);
|
||||
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_deserialize_nok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_bad_toml();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
assert!(deserializer.deserialize(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_serialize() {
|
||||
let mut cfg: UserConfig = UserConfig::default();
|
||||
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
|
||||
// Insert key
|
||||
cfg.remote.ssh_keys.insert(
|
||||
String::from("192.168.1.31"),
|
||||
PathBuf::from("/home/omar/.ssh/id_rsa"),
|
||||
);
|
||||
// Serialize
|
||||
let serializer: ConfigSerializer = ConfigSerializer {};
|
||||
let writer: Box<dyn Write> = Box::new(std::fs::File::create(toml_file.path()).unwrap());
|
||||
assert!(serializer.serialize(writer, &cfg).is_ok());
|
||||
// Reload configuration and check if it's ok
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
assert!(serializer.deserialize(Box::new(toml_file)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_fail_write() {
|
||||
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
|
||||
let writer: Box<dyn Write> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
|
||||
// Try to write unexisting file
|
||||
let serializer: ConfigSerializer = ConfigSerializer {};
|
||||
let cfg: UserConfig = UserConfig::default();
|
||||
assert!(serializer.serialize(writer, &cfg).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_fail_read() {
|
||||
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
|
||||
let reader: Box<dyn Read> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
|
||||
// Try to write unexisting file
|
||||
let serializer: ConfigSerializer = ConfigSerializer {};
|
||||
assert!(serializer.deserialize(reader).is_err());
|
||||
}
|
||||
|
||||
fn create_good_toml() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SCP"
|
||||
text_editor = "vim"
|
||||
show_hidden_files = true
|
||||
check_for_updates = true
|
||||
group_dirs = "last"
|
||||
file_fmt = "{NAME} {PEX}"
|
||||
remote_file_fmt = "{NAME} {USER}"
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_good_toml_no_opts() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SCP"
|
||||
text_editor = "vim"
|
||||
show_hidden_files = true
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_bad_toml() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SFTP"
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
}
|
||||
267
src/config/themes.rs
Normal file
267
src/config/themes.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
//! ## Themes
|
||||
//!
|
||||
//! `themes` is the module which provides the themes configurations and the serializers
|
||||
|
||||
/**
|
||||
* 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::fmt_color;
|
||||
use crate::utils::parser::parse_color;
|
||||
// ext
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use tuirealm::tui::style::Color;
|
||||
|
||||
/// ### Theme
|
||||
///
|
||||
/// Theme contains all the colors lookup table for termscp
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct Theme {
|
||||
// -- auth
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub auth_address: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub auth_bookmarks: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub auth_password: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub auth_port: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub auth_protocol: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub auth_recents: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub auth_username: Color,
|
||||
// -- misc
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub misc_error_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub misc_input_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub misc_keys: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub misc_quit_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub misc_save_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub misc_warn_dialog: Color,
|
||||
// -- transfer
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_local_explorer_background: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_local_explorer_foreground: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_local_explorer_highlighted: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_log_background: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_log_window: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_progress_bar_full: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_progress_bar_partial: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_remote_explorer_background: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_remote_explorer_foreground: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_remote_explorer_highlighted: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_status_hidden: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_status_sorting: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_status_sync_browsing: Color,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auth_address: Color::Yellow,
|
||||
auth_bookmarks: Color::LightGreen,
|
||||
auth_password: Color::LightBlue,
|
||||
auth_port: Color::LightCyan,
|
||||
auth_protocol: Color::LightGreen,
|
||||
auth_recents: Color::LightBlue,
|
||||
auth_username: Color::LightMagenta,
|
||||
misc_error_dialog: Color::Red,
|
||||
misc_input_dialog: Color::Reset,
|
||||
misc_keys: Color::Cyan,
|
||||
misc_quit_dialog: Color::Yellow,
|
||||
misc_save_dialog: Color::LightCyan,
|
||||
misc_warn_dialog: Color::LightRed,
|
||||
transfer_local_explorer_background: Color::Reset,
|
||||
transfer_local_explorer_foreground: Color::Reset,
|
||||
transfer_local_explorer_highlighted: Color::Yellow,
|
||||
transfer_log_background: Color::Reset,
|
||||
transfer_log_window: Color::LightGreen,
|
||||
transfer_progress_bar_partial: Color::Green,
|
||||
transfer_progress_bar_full: Color::Green,
|
||||
transfer_remote_explorer_background: Color::Reset,
|
||||
transfer_remote_explorer_foreground: Color::Reset,
|
||||
transfer_remote_explorer_highlighted: Color::LightBlue,
|
||||
transfer_status_hidden: Color::LightBlue,
|
||||
transfer_status_sorting: Color::LightYellow,
|
||||
transfer_status_sync_browsing: Color::LightGreen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- deserializer
|
||||
|
||||
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: &str = Deserialize::deserialize(deserializer)?;
|
||||
// Parse color
|
||||
match parse_color(s) {
|
||||
None => Err(DeError::custom("Invalid color")),
|
||||
Some(color) => Ok(color),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_color<S>(color: &Color, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// Convert color to string
|
||||
let s: String = fmt_color(color);
|
||||
serializer.serialize_str(s.as_str())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_config_themes_default() {
|
||||
let theme: Theme = Theme::default();
|
||||
assert_eq!(theme.auth_address, Color::Yellow);
|
||||
assert_eq!(theme.auth_bookmarks, Color::LightGreen);
|
||||
assert_eq!(theme.auth_password, Color::LightBlue);
|
||||
assert_eq!(theme.auth_port, Color::LightCyan);
|
||||
assert_eq!(theme.auth_protocol, Color::LightGreen);
|
||||
assert_eq!(theme.auth_recents, Color::LightBlue);
|
||||
assert_eq!(theme.auth_username, Color::LightMagenta);
|
||||
assert_eq!(theme.misc_error_dialog, Color::Red);
|
||||
assert_eq!(theme.misc_input_dialog, Color::Reset);
|
||||
assert_eq!(theme.misc_keys, Color::Cyan);
|
||||
assert_eq!(theme.misc_quit_dialog, Color::Yellow);
|
||||
assert_eq!(theme.misc_save_dialog, Color::LightCyan);
|
||||
assert_eq!(theme.misc_warn_dialog, Color::LightRed);
|
||||
assert_eq!(theme.transfer_local_explorer_background, Color::Reset);
|
||||
assert_eq!(theme.transfer_local_explorer_foreground, Color::Reset);
|
||||
assert_eq!(theme.transfer_local_explorer_highlighted, Color::Yellow);
|
||||
assert_eq!(theme.transfer_log_background, Color::Reset);
|
||||
assert_eq!(theme.transfer_log_window, Color::LightGreen);
|
||||
assert_eq!(theme.transfer_progress_bar_full, Color::Green);
|
||||
assert_eq!(theme.transfer_progress_bar_partial, Color::Green);
|
||||
assert_eq!(theme.transfer_remote_explorer_background, Color::Reset);
|
||||
assert_eq!(theme.transfer_remote_explorer_foreground, Color::Reset);
|
||||
assert_eq!(theme.transfer_remote_explorer_highlighted, Color::LightBlue);
|
||||
assert_eq!(theme.transfer_status_hidden, Color::LightBlue);
|
||||
assert_eq!(theme.transfer_status_sorting, Color::LightYellow);
|
||||
assert_eq!(theme.transfer_status_sync_browsing, Color::LightGreen);
|
||||
}
|
||||
}
|
||||
@@ -25,13 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Dependencies
|
||||
extern crate chrono;
|
||||
extern crate ftp4;
|
||||
#[cfg(os_target = "windows")]
|
||||
extern crate path_slash;
|
||||
extern crate regex;
|
||||
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// dependencies
|
||||
extern crate wildmatch;
|
||||
// locals
|
||||
use crate::fs::{FsEntry, FsFile};
|
||||
// ext
|
||||
@@ -36,9 +34,12 @@ use thiserror::Error;
|
||||
use wildmatch::WildMatch;
|
||||
// exports
|
||||
pub mod ftp_transfer;
|
||||
pub mod params;
|
||||
pub mod scp_transfer;
|
||||
pub mod sftp_transfer;
|
||||
|
||||
pub use params::FileTransferParams;
|
||||
|
||||
/// ## FileTransferProtocol
|
||||
///
|
||||
/// This enum defines the different transfer protocol available in termscp
|
||||
|
||||
137
src/filetransfer/params.rs
Normal file
137
src/filetransfer/params.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
//! ## Params
|
||||
//!
|
||||
//! file transfer parameters
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
use super::FileTransferProtocol;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// ### FileTransferParams
|
||||
///
|
||||
/// Holds connection parameters for file transfers
|
||||
#[derive(Clone)]
|
||||
pub struct FileTransferParams {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub entry_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl FileTransferParams {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new `FileTransferParams`
|
||||
pub fn new<S: AsRef<str>>(address: S) -> Self {
|
||||
Self {
|
||||
address: address.as_ref().to_string(),
|
||||
port: 22,
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: None,
|
||||
password: None,
|
||||
entry_directory: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### port
|
||||
///
|
||||
/// Set port for params
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// ### protocol
|
||||
///
|
||||
/// Set protocol for params
|
||||
pub fn protocol(mut self, protocol: FileTransferProtocol) -> Self {
|
||||
self.protocol = protocol;
|
||||
self
|
||||
}
|
||||
|
||||
/// ### username
|
||||
///
|
||||
/// Set username for params
|
||||
pub fn username<S: AsRef<str>>(mut self, username: Option<S>) -> Self {
|
||||
self.username = username.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// ### password
|
||||
///
|
||||
/// Set password for params
|
||||
pub fn password<S: AsRef<str>>(mut self, password: Option<S>) -> Self {
|
||||
self.password = password.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// ### entry_directory
|
||||
///
|
||||
/// Set entry directory
|
||||
pub fn entry_directory<P: AsRef<Path>>(mut self, dir: Option<P>) -> Self {
|
||||
self.entry_directory = dir.map(|x| x.as_ref().to_path_buf());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileTransferParams {
|
||||
fn default() -> Self {
|
||||
Self::new("localhost")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_params() {
|
||||
let params: FileTransferParams = FileTransferParams::new("test.rebex.net")
|
||||
.port(2222)
|
||||
.protocol(FileTransferProtocol::Scp)
|
||||
.username(Some("omar"))
|
||||
.password(Some("foobar"))
|
||||
.entry_directory(Some(&Path::new("/tmp")));
|
||||
assert_eq!(params.address.as_str(), "test.rebex.net");
|
||||
assert_eq!(params.port, 2222);
|
||||
assert_eq!(params.protocol, FileTransferProtocol::Scp);
|
||||
assert_eq!(params.username.as_ref().unwrap(), "omar");
|
||||
assert_eq!(params.password.as_ref().unwrap(), "foobar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_params_default() {
|
||||
let params: FileTransferParams = FileTransferParams::default();
|
||||
assert_eq!(params.address.as_str(), "localhost");
|
||||
assert_eq!(params.port, 22);
|
||||
assert_eq!(params.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(params.username.is_none());
|
||||
assert!(params.password.is_none());
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Dependencies
|
||||
#[cfg(os_target = "windows")]
|
||||
extern crate path_slash;
|
||||
extern crate regex;
|
||||
extern crate ssh2;
|
||||
|
||||
// Locals
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Dependencies
|
||||
extern crate ssh2;
|
||||
|
||||
// Locals
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
|
||||
@@ -25,11 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
extern crate bytesize;
|
||||
extern crate regex;
|
||||
#[cfg(target_family = "unix")]
|
||||
extern crate users;
|
||||
// Locals
|
||||
use super::FsEntry;
|
||||
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
|
||||
@@ -321,13 +316,13 @@ impl Formatter {
|
||||
};
|
||||
let name: &str = fsentry.get_name();
|
||||
let last_idx: usize = match fsentry.is_dir() {
|
||||
// NOTE: For directories is 19, since we push '/' to name
|
||||
true => file_len - 5,
|
||||
false => file_len - 4,
|
||||
// NOTE: For directories is l - 2, since we push '/' to name
|
||||
true => file_len - 2,
|
||||
false => file_len - 1,
|
||||
};
|
||||
let mut name: String = match name.len() >= file_len {
|
||||
false => name.to_string(),
|
||||
true => format!("{}...", &name[0..last_idx]),
|
||||
true => format!("{}…", &name[0..last_idx]),
|
||||
};
|
||||
if fsentry.is_dir() {
|
||||
name.push('/');
|
||||
@@ -640,7 +635,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperu... -rw-r--r-- root 8.2 KB {}",
|
||||
"piroparoporoperoperupup… -rw-r--r-- root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -648,7 +643,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperu... -rw-r--r-- 0 8.2 KB {}",
|
||||
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
// Mods
|
||||
pub(crate) mod builder;
|
||||
mod formatter;
|
||||
// Deps
|
||||
extern crate bitflags;
|
||||
// Locals
|
||||
use super::FsEntry;
|
||||
use formatter::Formatter;
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// dependencies
|
||||
extern crate wildmatch;
|
||||
// ext
|
||||
use std::fs::{self, File, Metadata, OpenOptions};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
26
src/lib.rs
26
src/lib.rs
@@ -32,19 +32,43 @@
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
extern crate bytesize;
|
||||
extern crate chrono;
|
||||
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;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate magic_crypt;
|
||||
extern crate open;
|
||||
#[cfg(target_os = "windows")]
|
||||
extern crate path_slash;
|
||||
extern crate rand;
|
||||
extern crate regex;
|
||||
extern crate ssh2;
|
||||
extern crate tempfile;
|
||||
extern crate textwrap;
|
||||
extern crate tuirealm;
|
||||
extern crate ureq;
|
||||
#[cfg(target_family = "unix")]
|
||||
extern crate users;
|
||||
extern crate whoami;
|
||||
extern crate wildmatch;
|
||||
|
||||
pub mod activity_manager;
|
||||
pub mod bookmarks;
|
||||
pub mod config;
|
||||
pub mod filetransfer;
|
||||
pub mod fs;
|
||||
pub mod host;
|
||||
pub mod support;
|
||||
pub mod system;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
|
||||
323
src/main.rs
323
src/main.rs
@@ -26,7 +26,7 @@ const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
|
||||
// Crates
|
||||
extern crate getopts;
|
||||
extern crate argh;
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
#[macro_use]
|
||||
@@ -38,182 +38,237 @@ extern crate magic_crypt;
|
||||
extern crate rpassword;
|
||||
|
||||
// External libs
|
||||
use getopts::Options;
|
||||
use argh::FromArgs;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
// Include
|
||||
mod activity_manager;
|
||||
mod bookmarks;
|
||||
mod config;
|
||||
mod filetransfer;
|
||||
mod fs;
|
||||
mod host;
|
||||
mod support;
|
||||
mod system;
|
||||
mod ui;
|
||||
mod utils;
|
||||
|
||||
// namespaces
|
||||
use activity_manager::{ActivityManager, NextActivity};
|
||||
use filetransfer::FileTransferProtocol;
|
||||
use filetransfer::FileTransferParams;
|
||||
use system::logging;
|
||||
|
||||
/// ### print_usage
|
||||
///
|
||||
/// Print usage
|
||||
enum Task {
|
||||
Activity(NextActivity),
|
||||
ImportTheme(PathBuf),
|
||||
}
|
||||
|
||||
fn print_usage(opts: Options) {
|
||||
let brief = String::from(
|
||||
"Usage: termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]",
|
||||
);
|
||||
print!("{}", opts.usage(&brief));
|
||||
println!("\nPlease, report issues to <https://github.com/veeso/termscp>");
|
||||
println!("Please, consider supporting the author <https://www.buymeacoffee.com/veeso>")
|
||||
#[derive(FromArgs)]
|
||||
#[argh(description = "
|
||||
where positional can be: [protocol://user@address:port:wrkdir] [local-wrkdir]
|
||||
|
||||
Please, report issues to <https://github.com/veeso/termscp>
|
||||
Please, consider supporting the author <https://www.buymeacoffee.com/veeso>")]
|
||||
struct Args {
|
||||
#[argh(switch, short = 'c', description = "open termscp configuration")]
|
||||
config: bool,
|
||||
#[argh(option, short = 'P', description = "provide password from CLI")]
|
||||
password: Option<String>,
|
||||
#[argh(switch, short = 'q', description = "disable logging")]
|
||||
quiet: bool,
|
||||
#[argh(option, short = 't', description = "import specified theme")]
|
||||
theme: Option<String>,
|
||||
#[argh(
|
||||
option,
|
||||
short = 'T',
|
||||
default = "10",
|
||||
description = "set UI ticks; default 10ms"
|
||||
)]
|
||||
ticks: u64,
|
||||
#[argh(switch, short = 'v', description = "print version")]
|
||||
version: bool,
|
||||
// -- positional
|
||||
#[argh(
|
||||
positional,
|
||||
description = "protocol://user@address:port:wrkdir local-wrkdir"
|
||||
)]
|
||||
positional: Vec<String>,
|
||||
}
|
||||
|
||||
struct RunOpts {
|
||||
remote: Option<FileTransferParams>,
|
||||
ticks: Duration,
|
||||
log_enabled: bool,
|
||||
task: Task,
|
||||
}
|
||||
|
||||
impl Default for RunOpts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
remote: None,
|
||||
ticks: Duration::from_millis(10),
|
||||
log_enabled: true,
|
||||
task: Task::Activity(NextActivity::Authentication),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
//Program CLI options
|
||||
let mut address: Option<String> = None; // None
|
||||
let mut port: u16 = 22; // Default port
|
||||
let mut username: Option<String> = None; // Default username
|
||||
let mut password: Option<String> = None; // Default password
|
||||
let mut remote_wrkdir: Option<PathBuf> = None;
|
||||
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
|
||||
let mut ticks: Duration = Duration::from_millis(10);
|
||||
let mut log_enabled: bool = true;
|
||||
//Process options
|
||||
let mut opts = Options::new();
|
||||
opts.optopt("P", "password", "Provide password from CLI", "<password>");
|
||||
opts.optopt("T", "ticks", "Set UI ticks; default 10ms", "<ms>");
|
||||
opts.optflag("q", "quiet", "Disable logging");
|
||||
opts.optflag("v", "version", "");
|
||||
opts.optflag("h", "help", "Print this menu");
|
||||
let matches = match opts.parse(&args[1..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => {
|
||||
println!("{}", f.to_string());
|
||||
let args: Args = argh::from_env();
|
||||
// Parse args
|
||||
let mut run_opts: RunOpts = match parse_args(args) {
|
||||
Ok(opts) => opts,
|
||||
Err(err) => {
|
||||
eprintln!("{}", err);
|
||||
std::process::exit(255);
|
||||
}
|
||||
};
|
||||
// Help
|
||||
if matches.opt_present("h") {
|
||||
print_usage(opts);
|
||||
std::process::exit(255);
|
||||
}
|
||||
// Version
|
||||
if matches.opt_present("v") {
|
||||
eprintln!(
|
||||
"termscp - {} - Developed by {}",
|
||||
TERMSCP_VERSION, TERMSCP_AUTHORS,
|
||||
);
|
||||
std::process::exit(255);
|
||||
}
|
||||
// Logging
|
||||
if matches.opt_present("q") {
|
||||
log_enabled = false;
|
||||
}
|
||||
// Match password
|
||||
if let Some(passwd) = matches.opt_str("P") {
|
||||
password = Some(passwd);
|
||||
}
|
||||
// Match ticks
|
||||
if let Some(val) = matches.opt_str("T") {
|
||||
match val.parse::<usize>() {
|
||||
Ok(val) => ticks = Duration::from_millis(val as u64),
|
||||
Err(_) => {
|
||||
eprintln!("Ticks is not a number '{}'", val);
|
||||
print_usage(opts);
|
||||
std::process::exit(255);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check free args
|
||||
let extra_args: Vec<String> = matches.free;
|
||||
// Remote argument
|
||||
if let Some(remote) = extra_args.get(0) {
|
||||
// Parse address
|
||||
match utils::parser::parse_remote_opt(remote) {
|
||||
Ok(host_opts) => {
|
||||
// Set params
|
||||
address = Some(host_opts.hostname);
|
||||
port = host_opts.port;
|
||||
protocol = host_opts.protocol;
|
||||
username = host_opts.username;
|
||||
remote_wrkdir = host_opts.wrkdir;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Bad address option: {}", err);
|
||||
print_usage(opts);
|
||||
std::process::exit(255);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Local directory
|
||||
if let Some(localdir) = extra_args.get(1) {
|
||||
// Change working directory if local dir is set
|
||||
let localdir: PathBuf = PathBuf::from(localdir);
|
||||
if let Err(err) = env::set_current_dir(localdir.as_path()) {
|
||||
eprintln!("Bad working directory argument: {}", err);
|
||||
std::process::exit(255);
|
||||
}
|
||||
}
|
||||
// Get working directory
|
||||
let wrkdir: PathBuf = match env::current_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => PathBuf::from("/"),
|
||||
};
|
||||
// Setup logging
|
||||
if log_enabled {
|
||||
if run_opts.log_enabled {
|
||||
if let Err(err) = logging::init() {
|
||||
eprintln!("Failed to initialize logging: {}", err);
|
||||
}
|
||||
}
|
||||
// Read password from remote
|
||||
if let Err(err) = read_password(&mut run_opts) {
|
||||
eprintln!("{}", err);
|
||||
std::process::exit(255);
|
||||
}
|
||||
info!("termscp {} started!", TERMSCP_VERSION);
|
||||
// Run
|
||||
info!("Starting activity manager...");
|
||||
let rc: i32 = run(run_opts);
|
||||
info!("termscp terminated");
|
||||
// Then return
|
||||
std::process::exit(rc);
|
||||
}
|
||||
|
||||
/// ### parse_args
|
||||
///
|
||||
/// Parse arguments
|
||||
/// In case of success returns `RunOpts`
|
||||
/// in case something is wrong returns the error message
|
||||
fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
let mut run_opts: RunOpts = RunOpts::default();
|
||||
// Version
|
||||
if args.version {
|
||||
return Err(format!(
|
||||
"termscp - {} - Developed by {}",
|
||||
TERMSCP_VERSION, TERMSCP_AUTHORS,
|
||||
));
|
||||
}
|
||||
// Setup activity?
|
||||
if args.config {
|
||||
run_opts.task = Task::Activity(NextActivity::SetupActivity);
|
||||
}
|
||||
// Logging
|
||||
if args.quiet {
|
||||
run_opts.log_enabled = false;
|
||||
}
|
||||
// Match ticks
|
||||
run_opts.ticks = Duration::from_millis(args.ticks);
|
||||
// @! extra modes
|
||||
if let Some(theme) = args.theme {
|
||||
run_opts.task = Task::ImportTheme(PathBuf::from(theme));
|
||||
}
|
||||
// @! Ordinary mode
|
||||
// Remote argument
|
||||
if let Some(remote) = args.positional.get(0) {
|
||||
// Parse address
|
||||
match utils::parser::parse_remote_opt(remote.as_str()) {
|
||||
Ok(mut remote) => {
|
||||
// If password is provided, set password
|
||||
if let Some(passwd) = args.password {
|
||||
remote = remote.password(Some(passwd));
|
||||
}
|
||||
// Set params
|
||||
run_opts.remote = Some(remote);
|
||||
// In this case the first activity will be FileTransfer
|
||||
run_opts.task = Task::Activity(NextActivity::FileTransfer);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(format!("Bad address option: {}", err));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Local directory
|
||||
if let Some(localdir) = args.positional.get(1) {
|
||||
// Change working directory if local dir is set
|
||||
let localdir: PathBuf = PathBuf::from(localdir);
|
||||
if let Err(err) = env::set_current_dir(localdir.as_path()) {
|
||||
return Err(format!("Bad working directory argument: {}", err));
|
||||
}
|
||||
}
|
||||
Ok(run_opts)
|
||||
}
|
||||
|
||||
/// ### read_password
|
||||
///
|
||||
/// Read password from tty if address is specified
|
||||
fn read_password(run_opts: &mut RunOpts) -> Result<(), String> {
|
||||
// Initialize client if necessary
|
||||
let mut start_activity: NextActivity = NextActivity::Authentication;
|
||||
if address.is_some() {
|
||||
debug!("User has specified remote options: address: {:?}, port: {:?}, protocol: {:?}, user: {:?}, password: {}", address, port, protocol, username, utils::fmt::shadow_password(password.as_deref().unwrap_or("")));
|
||||
if password.is_none() {
|
||||
if let Some(remote) = run_opts.remote.as_mut() {
|
||||
debug!("User has specified remote options: address: {:?}, port: {:?}, protocol: {:?}, user: {:?}, password: {}", remote.address, remote.port, remote.protocol, remote.username, utils::fmt::shadow_password(remote.password.as_deref().unwrap_or("")));
|
||||
if remote.password.is_none() {
|
||||
// Ask password if unspecified
|
||||
password = match rpassword::read_password_from_tty(Some("Password: ")) {
|
||||
remote.password = match rpassword::read_password_from_tty(Some("Password: ")) {
|
||||
Ok(p) => {
|
||||
if p.is_empty() {
|
||||
None
|
||||
} else {
|
||||
debug!(
|
||||
"Read password from tty: {}",
|
||||
utils::fmt::shadow_password(p.as_str())
|
||||
);
|
||||
Some(p)
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Could not read password from prompt");
|
||||
std::process::exit(255);
|
||||
return Err("Could not read password from prompt".to_string());
|
||||
}
|
||||
};
|
||||
debug!(
|
||||
"Read password from tty: {}",
|
||||
utils::fmt::shadow_password(password.as_deref().unwrap_or(""))
|
||||
);
|
||||
}
|
||||
// In this case the first activity will be FileTransfer
|
||||
start_activity = NextActivity::FileTransfer;
|
||||
}
|
||||
// Create activity manager (and context too)
|
||||
let mut manager: ActivityManager = match ActivityManager::new(&wrkdir, ticks) {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
eprintln!("Could not start activity manager: {}", err);
|
||||
std::process::exit(255);
|
||||
}
|
||||
};
|
||||
// Set file transfer params if set
|
||||
if let Some(address) = address {
|
||||
manager.set_filetransfer_params(address, port, protocol, username, password, remote_wrkdir);
|
||||
}
|
||||
// Run
|
||||
info!("Starting activity manager...");
|
||||
manager.run(start_activity);
|
||||
info!("termscp terminated");
|
||||
// Then return
|
||||
std::process::exit(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### run
|
||||
///
|
||||
/// Run task and return rc
|
||||
fn run(mut run_opts: RunOpts) -> i32 {
|
||||
match run_opts.task {
|
||||
Task::ImportTheme(theme) => match support::import_theme(theme.as_path()) {
|
||||
Ok(_) => {
|
||||
println!("Theme has been successfully imported!");
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{}", err);
|
||||
1
|
||||
}
|
||||
},
|
||||
Task::Activity(activity) => {
|
||||
// Get working directory
|
||||
let wrkdir: PathBuf = match env::current_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => PathBuf::from("/"),
|
||||
};
|
||||
// Create activity manager (and context too)
|
||||
let mut manager: ActivityManager =
|
||||
match ActivityManager::new(wrkdir.as_path(), run_opts.ticks) {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
eprintln!("Could not start activity manager: {}", err);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
// Set file transfer params if set
|
||||
if let Some(remote) = run_opts.remote.take() {
|
||||
manager.set_filetransfer_params(remote);
|
||||
}
|
||||
manager.run(activity);
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
src/support.rs
Normal file
68
src/support.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! ## Support
|
||||
//!
|
||||
//! this module exposes some extra run modes for termscp, meant to be used for "support", such as installing themes
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
// mod
|
||||
use crate::system::{environment, theme_provider::ThemeProvider};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// ### import_theme
|
||||
///
|
||||
/// Import theme at provided path into termscp
|
||||
pub fn import_theme(p: &Path) -> Result<(), String> {
|
||||
if !p.exists() {
|
||||
return Err(String::from(
|
||||
"Could not import theme: No such file or directory",
|
||||
));
|
||||
}
|
||||
// Validate theme file
|
||||
ThemeProvider::new(p).map_err(|e| format!("Invalid theme error: {}", e))?;
|
||||
// get config dir
|
||||
let cfg_dir: PathBuf = get_config_dir()?;
|
||||
// Get theme directory
|
||||
let theme_file: PathBuf = environment::get_theme_path(cfg_dir.as_path());
|
||||
// Copy theme to theme_dir
|
||||
fs::copy(p, theme_file.as_path())
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("Could not import theme: {}", e))
|
||||
}
|
||||
|
||||
/// ### get_config_dir
|
||||
///
|
||||
/// Get configuration directory
|
||||
fn get_config_dir() -> Result<PathBuf, String> {
|
||||
match environment::init_config_dir() {
|
||||
Ok(Some(config_dir)) => Ok(config_dir),
|
||||
Ok(None) => Err(String::from(
|
||||
"Your system doesn't provide a configuration directory",
|
||||
)),
|
||||
Err(err) => Err(format!(
|
||||
"Could not initialize configuration directory: {}",
|
||||
err
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -25,15 +25,15 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
extern crate whoami;
|
||||
// Crate
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(feature = "with-keyring")]
|
||||
use super::keys::keyringstorage::KeyringStorage;
|
||||
use super::keys::{filestorage::FileStorage, KeyStorage, KeyStorageError};
|
||||
// Local
|
||||
use crate::bookmarks::serializer::BookmarkSerializer;
|
||||
use crate::bookmarks::{Bookmark, SerializerError, SerializerErrorKind, UserHosts};
|
||||
use crate::config::{
|
||||
bookmarks::{Bookmark, UserHosts},
|
||||
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
||||
};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::utils::crypto;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
@@ -67,10 +67,10 @@ impl BookmarksClient {
|
||||
recents_size: usize,
|
||||
) -> Result<BookmarksClient, SerializerError> {
|
||||
// Create default hosts
|
||||
let default_hosts: UserHosts = Default::default();
|
||||
let default_hosts: UserHosts = UserHosts::default();
|
||||
debug!("Setting up bookmarks client...");
|
||||
// Make a key storage (windows / macos)
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
// Make a key storage (with-keyring)
|
||||
#[cfg(feature = "with-keyring")]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
debug!("Setting up KeyStorage");
|
||||
let username: String = whoami::username();
|
||||
@@ -91,13 +91,8 @@ impl BookmarksClient {
|
||||
}
|
||||
}
|
||||
};
|
||||
// Make a key storage (linux / unix)
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "netbsd"
|
||||
))]
|
||||
// Make a key storage (wno-keyring)
|
||||
#[cfg(not(feature = "with-keyring"))]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "bookmarks";
|
||||
@@ -329,10 +324,7 @@ impl BookmarksClient {
|
||||
.truncate(true)
|
||||
.open(self.bookmarks_file.as_path())
|
||||
{
|
||||
Ok(writer) => {
|
||||
let serializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
serializer.serialize(Box::new(writer), &self.hosts)
|
||||
}
|
||||
Ok(writer) => serialize(&self.hosts, Box::new(writer)),
|
||||
Err(err) => {
|
||||
error!("Failed to write bookmarks: {}", err);
|
||||
Err(SerializerError::new_ex(
|
||||
@@ -355,8 +347,7 @@ impl BookmarksClient {
|
||||
{
|
||||
Ok(reader) => {
|
||||
// Deserialize
|
||||
let deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
match deserializer.deserialize(Box::new(reader)) {
|
||||
match deserialize(Box::new(reader)) {
|
||||
Ok(hosts) => {
|
||||
self.hosts = hosts;
|
||||
Ok(())
|
||||
@@ -455,7 +446,7 @@ mod tests {
|
||||
target_os = "linux",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "netbsd"
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
fn test_system_bookmarks_new_err() {
|
||||
assert!(BookmarksClient::new(
|
||||
@@ -709,6 +700,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_bookmarks_decrypt_str() {
|
||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
client.key = "MYSUPERSECRETKEY".to_string();
|
||||
assert_eq!(
|
||||
client.decrypt_str("z4Z6LpcpYqBW4+bkIok+5A==").ok().unwrap(),
|
||||
"Hello world!"
|
||||
);
|
||||
assert!(client.decrypt_str("bidoof").is_err());
|
||||
}
|
||||
|
||||
/// ### get_paths
|
||||
///
|
||||
/// Get paths for configuration and key for bookmarks
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
extern crate rand;
|
||||
// Locals
|
||||
use crate::config::serializer::ConfigSerializer;
|
||||
use crate::config::{SerializerError, SerializerErrorKind, UserConfig};
|
||||
use crate::config::{
|
||||
params::UserConfig,
|
||||
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
||||
};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs;
|
||||
// Ext
|
||||
@@ -49,13 +49,14 @@ pub struct ConfigClient {
|
||||
config: UserConfig, // Configuration loaded
|
||||
config_path: PathBuf, // Configuration TOML Path
|
||||
ssh_key_dir: PathBuf, // SSH Key storage directory
|
||||
degraded: bool, // Indicates the `ConfigClient` is working in degraded mode
|
||||
}
|
||||
|
||||
impl ConfigClient {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new `ConfigClient` with provided path
|
||||
pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result<ConfigClient, SerializerError> {
|
||||
pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result<Self, SerializerError> {
|
||||
// Initialize a default configuration
|
||||
let default_config: UserConfig = UserConfig::default();
|
||||
info!(
|
||||
@@ -68,6 +69,7 @@ impl ConfigClient {
|
||||
config: default_config,
|
||||
config_path: PathBuf::from(config_path),
|
||||
ssh_key_dir: PathBuf::from(ssh_key_dir),
|
||||
degraded: false,
|
||||
};
|
||||
// If ssh key directory doesn't exist, create it
|
||||
if !ssh_key_dir.exists() {
|
||||
@@ -102,6 +104,20 @@ impl ConfigClient {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// ### degraded
|
||||
///
|
||||
/// Instantiate a ConfigClient in degraded mode.
|
||||
/// When in degraded mode, the configuration in use will be the default configuration
|
||||
/// and the IO operation on configuration won't be available
|
||||
pub fn degraded() -> Self {
|
||||
Self {
|
||||
config: UserConfig::default(),
|
||||
config_path: PathBuf::default(),
|
||||
ssh_key_dir: PathBuf::default(),
|
||||
degraded: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Text editor
|
||||
|
||||
/// ### get_text_editor
|
||||
@@ -234,6 +250,12 @@ impl ConfigClient {
|
||||
username: &str,
|
||||
ssh_key: &str,
|
||||
) -> Result<(), SerializerError> {
|
||||
if self.degraded {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::GenericError,
|
||||
String::from("Configuration won't be saved, since in degraded mode"),
|
||||
));
|
||||
}
|
||||
let host_name: String = Self::make_ssh_host_key(host, username);
|
||||
// Get key path
|
||||
let ssh_key_path: PathBuf = {
|
||||
@@ -267,6 +289,12 @@ impl ConfigClient {
|
||||
/// This operation also unlinks the key file in `ssh_key_dir`
|
||||
/// and also commits changes to configuration, to prevent incoerent data
|
||||
pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> {
|
||||
if self.degraded {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::GenericError,
|
||||
String::from("Configuration won't be saved, since in degraded mode"),
|
||||
));
|
||||
}
|
||||
// Remove key from configuration and get key path
|
||||
info!("Removing key for {}@{}", host, username);
|
||||
let key_path: PathBuf = match self
|
||||
@@ -293,6 +321,9 @@ impl ConfigClient {
|
||||
/// None is returned if key doesn't exist
|
||||
/// `std::io::Error` is returned in case it was not possible to read the key file
|
||||
pub fn get_ssh_key(&self, mkey: &str) -> std::io::Result<Option<SshHost>> {
|
||||
if self.degraded {
|
||||
return Ok(None);
|
||||
}
|
||||
// Check if Key exists
|
||||
match self.config.remote.ssh_keys.get(mkey) {
|
||||
None => Ok(None),
|
||||
@@ -318,6 +349,12 @@ impl ConfigClient {
|
||||
///
|
||||
/// Write configuration to file
|
||||
pub fn write_config(&self) -> Result<(), SerializerError> {
|
||||
if self.degraded {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::GenericError,
|
||||
String::from("Configuration won't be saved, since in degraded mode"),
|
||||
));
|
||||
}
|
||||
// Open file
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
@@ -325,10 +362,7 @@ impl ConfigClient {
|
||||
.truncate(true)
|
||||
.open(self.config_path.as_path())
|
||||
{
|
||||
Ok(writer) => {
|
||||
let serializer: ConfigSerializer = ConfigSerializer {};
|
||||
serializer.serialize(Box::new(writer), &self.config)
|
||||
}
|
||||
Ok(writer) => serialize(&self.config, Box::new(writer)),
|
||||
Err(err) => {
|
||||
error!("Failed to write configuration file: {}", err);
|
||||
Err(SerializerError::new_ex(
|
||||
@@ -343,6 +377,12 @@ impl ConfigClient {
|
||||
///
|
||||
/// Read configuration from file (or reload it if already read)
|
||||
pub fn read_config(&mut self) -> Result<(), SerializerError> {
|
||||
if self.degraded {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::GenericError,
|
||||
String::from("Configuration won't be loaded, since in degraded mode"),
|
||||
));
|
||||
}
|
||||
// Open bookmarks file for read
|
||||
match OpenOptions::new()
|
||||
.read(true)
|
||||
@@ -350,8 +390,7 @@ impl ConfigClient {
|
||||
{
|
||||
Ok(reader) => {
|
||||
// Deserialize
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
match deserializer.deserialize(Box::new(reader)) {
|
||||
match deserialize(Box::new(reader)) {
|
||||
Ok(config) => {
|
||||
self.config = config;
|
||||
Ok(())
|
||||
@@ -419,6 +458,7 @@ mod tests {
|
||||
.unwrap();
|
||||
// Verify parameters
|
||||
let default_config: UserConfig = UserConfig::default();
|
||||
assert_eq!(client.degraded, false);
|
||||
assert_eq!(client.config.remote.ssh_keys.len(), 0);
|
||||
assert_eq!(
|
||||
client.config.user_interface.default_protocol,
|
||||
@@ -432,6 +472,20 @@ mod tests {
|
||||
assert_eq!(client.ssh_key_dir, ssh_keys_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_degraded() {
|
||||
let mut client: ConfigClient = ConfigClient::degraded();
|
||||
assert_eq!(client.degraded, true);
|
||||
assert_eq!(client.config_path, PathBuf::default());
|
||||
assert_eq!(client.ssh_key_dir, PathBuf::default());
|
||||
// I/O
|
||||
assert!(client.add_ssh_key("Omar", "omar", "omar").is_err());
|
||||
assert!(client.del_ssh_key("omar", "omar").is_err());
|
||||
assert!(client.get_ssh_key("omar").ok().unwrap().is_none());
|
||||
assert!(client.write_config().is_err());
|
||||
assert!(client.read_config().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_new_err() {
|
||||
assert!(
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
extern crate dirs;
|
||||
|
||||
// Ext
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -96,6 +93,17 @@ pub fn get_log_paths(config_dir: &Path) -> PathBuf {
|
||||
log_file
|
||||
}
|
||||
|
||||
/// ### get_theme_path
|
||||
///
|
||||
/// Get paths for theme provider
|
||||
/// Returns: path of theme.toml
|
||||
pub fn get_theme_path(config_dir: &Path) -> PathBuf {
|
||||
// Prepare paths
|
||||
let mut theme_file: PathBuf = PathBuf::from(config_dir);
|
||||
theme_file.push("theme.toml");
|
||||
theme_file
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -160,4 +168,12 @@ mod tests {
|
||||
PathBuf::from("/home/omar/.config/termscp/termscp.log"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_environment_get_theme_path() {
|
||||
assert_eq!(
|
||||
get_theme_path(&Path::new("/home/omar/.config/termscp/")),
|
||||
PathBuf::from("/home/omar/.config/termscp/theme.toml"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
extern crate keyring;
|
||||
// Local
|
||||
use super::{KeyStorage, KeyStorageError};
|
||||
// Ext
|
||||
@@ -39,7 +37,6 @@ pub struct KeyringStorage {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl KeyringStorage {
|
||||
/// ### new
|
||||
///
|
||||
@@ -51,7 +48,6 @@ impl KeyringStorage {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl KeyStorage for KeyringStorage {
|
||||
/// ### get_key
|
||||
///
|
||||
@@ -68,7 +64,10 @@ impl KeyStorage for KeyringStorage {
|
||||
KeyringError::WindowsVaultError => Err(KeyStorageError::NoSuchKey),
|
||||
#[cfg(target_os = "macos")]
|
||||
KeyringError::MacOsKeychainError(_) => Err(KeyStorageError::NoSuchKey),
|
||||
_ => panic!("{}", e),
|
||||
#[cfg(target_os = "linux")]
|
||||
KeyringError::SecretServiceError(_) => Err(KeyStorageError::ProviderError),
|
||||
KeyringError::Parse(_) => Err(KeyStorageError::BadSytax),
|
||||
_ => Err(KeyStorageError::ProviderError),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -93,15 +92,19 @@ impl KeyStorage for KeyringStorage {
|
||||
// Check what kind of error is returned
|
||||
match storage.get_password() {
|
||||
Ok(_) => true,
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
Err(err) => !matches!(err, KeyringError::NoBackendFound),
|
||||
#[cfg(target_os = "linux")]
|
||||
Err(err) => !matches!(
|
||||
err,
|
||||
KeyringError::NoBackendFound | KeyringError::SecretServiceError(_)
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
extern crate whoami;
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -27,30 +27,25 @@
|
||||
*/
|
||||
// Storages
|
||||
pub mod filestorage;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(feature = "with-keyring")]
|
||||
pub mod keyringstorage;
|
||||
// ext
|
||||
use thiserror::Error;
|
||||
|
||||
/// ## KeyStorageError
|
||||
///
|
||||
/// defines the error type for the `KeyStorage`
|
||||
#[derive(PartialEq, std::fmt::Debug)]
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
pub enum KeyStorageError {
|
||||
//BadKey,
|
||||
#[cfg(feature = "with-keyring")]
|
||||
#[error("Key has a bad syntax")]
|
||||
BadSytax,
|
||||
#[error("Provider service error")]
|
||||
ProviderError,
|
||||
#[error("No such key")]
|
||||
NoSuchKey,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeyStorageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let err: String = String::from(match &self {
|
||||
//KeyStorageError::BadKey => "Bad key syntax",
|
||||
KeyStorageError::ProviderError => "Provider service error",
|
||||
KeyStorageError::NoSuchKey => "No such key",
|
||||
});
|
||||
write!(f, "{}", err)
|
||||
}
|
||||
}
|
||||
|
||||
/// ## KeyStorage
|
||||
///
|
||||
/// this traits provides the methods to communicate and interact with the key storage.
|
||||
@@ -82,12 +77,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_mod_errors() {
|
||||
#[cfg(feature = "with-keyring")]
|
||||
assert_eq!(
|
||||
format!("{}", KeyStorageError::ProviderError),
|
||||
KeyStorageError::BadSytax.to_string(),
|
||||
String::from("Key has a bad syntax")
|
||||
);
|
||||
assert_eq!(
|
||||
KeyStorageError::ProviderError.to_string(),
|
||||
String::from("Provider service error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", KeyStorageError::NoSuchKey),
|
||||
KeyStorageError::NoSuchKey.to_string(),
|
||||
String::from("No such key")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
pub mod bookmarks_client;
|
||||
pub mod config_client;
|
||||
pub mod environment;
|
||||
pub(crate) mod keys;
|
||||
pub(self) mod keys;
|
||||
pub mod logging;
|
||||
pub mod sshkey_storage;
|
||||
pub mod theme_provider;
|
||||
|
||||
@@ -67,6 +67,7 @@ impl SshKeyStorage {
|
||||
/// ### empty
|
||||
///
|
||||
/// Create an empty ssh key storage; used in case `ConfigClient` is not available
|
||||
#[cfg(test)]
|
||||
pub fn empty() -> Self {
|
||||
SshKeyStorage {
|
||||
hosts: HashMap::new(),
|
||||
|
||||
246
src/system/theme_provider.rs
Normal file
246
src/system/theme_provider.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
//! ## ThemeProvider
|
||||
//!
|
||||
//! `theme_provider` is the module which provides an API between the theme configuration and the system
|
||||
|
||||
/**
|
||||
* 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::config::{
|
||||
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
||||
themes::Theme,
|
||||
};
|
||||
// Ext
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::string::ToString;
|
||||
|
||||
/// ## ThemeProvider
|
||||
///
|
||||
/// ThemeProvider provides a high level API to communicate with the termscp theme
|
||||
pub struct ThemeProvider {
|
||||
theme: Theme, // Theme loaded
|
||||
theme_path: PathBuf, // Theme TOML Path
|
||||
degraded: bool, // Fallback mode; won't work with file system
|
||||
}
|
||||
|
||||
impl ThemeProvider {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new `ThemeProvider`
|
||||
pub fn new(theme_path: &Path) -> Result<Self, SerializerError> {
|
||||
let default_theme: Theme = Theme::default();
|
||||
info!(
|
||||
"Setting up theme provider with thene path {} ",
|
||||
theme_path.display(),
|
||||
);
|
||||
// Create provider
|
||||
let mut provider: ThemeProvider = ThemeProvider {
|
||||
theme: default_theme,
|
||||
theme_path: theme_path.to_path_buf(),
|
||||
degraded: false,
|
||||
};
|
||||
// If Config file doesn't exist, create it
|
||||
if !theme_path.exists() {
|
||||
if let Err(err) = provider.save() {
|
||||
error!("Couldn't write theme file: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Theme file didn't exist; created file");
|
||||
} else {
|
||||
// otherwise Load configuration from file
|
||||
if let Err(err) = provider.load() {
|
||||
error!("Couldn't read thene file: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Read theme file");
|
||||
}
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
/// ### degraded
|
||||
///
|
||||
/// Create a new theme provider which won't work with file system.
|
||||
/// This is done in order to prevent a lot of `unwrap_or` on Ui
|
||||
pub fn degraded() -> Self {
|
||||
Self {
|
||||
theme: Theme::default(),
|
||||
theme_path: PathBuf::default(),
|
||||
degraded: true,
|
||||
}
|
||||
}
|
||||
|
||||
// -- getters
|
||||
|
||||
/// ### theme
|
||||
///
|
||||
/// Returns theme as reference
|
||||
pub fn theme(&self) -> &Theme {
|
||||
&self.theme
|
||||
}
|
||||
|
||||
/// ### theme_mut
|
||||
///
|
||||
/// Returns a mutable reference to the theme
|
||||
pub fn theme_mut(&mut self) -> &mut Theme {
|
||||
&mut self.theme
|
||||
}
|
||||
|
||||
// -- io
|
||||
|
||||
/// ### load
|
||||
///
|
||||
/// Load theme from file
|
||||
pub fn load(&mut self) -> Result<(), SerializerError> {
|
||||
if self.degraded {
|
||||
warn!("Configuration won't be loaded, since degraded; reloading default...");
|
||||
self.theme = Theme::default();
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::GenericError,
|
||||
String::from("Can't access theme file"),
|
||||
));
|
||||
}
|
||||
// Open theme file for read
|
||||
debug!("Loading theme from file...");
|
||||
match OpenOptions::new()
|
||||
.read(true)
|
||||
.open(self.theme_path.as_path())
|
||||
{
|
||||
Ok(reader) => {
|
||||
// Deserialize
|
||||
match deserialize(Box::new(reader)) {
|
||||
Ok(theme) => {
|
||||
self.theme = theme;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to read theme: {}", err);
|
||||
Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### save
|
||||
///
|
||||
/// Save theme to file
|
||||
pub fn save(&self) -> Result<(), SerializerError> {
|
||||
if self.degraded {
|
||||
warn!("Configuration won't be saved, since in degraded mode");
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::GenericError,
|
||||
String::from("Can't access theme file"),
|
||||
));
|
||||
}
|
||||
// Open file
|
||||
debug!("Writing theme");
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(self.theme_path.as_path())
|
||||
{
|
||||
Ok(writer) => serialize(self.theme(), Box::new(writer)),
|
||||
Err(err) => {
|
||||
error!("Failed to write theme: {}", err);
|
||||
Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tuirealm::tui::style::Color;
|
||||
|
||||
#[test]
|
||||
fn test_system_theme_provider_new() {
|
||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||
let theme_path: PathBuf = get_theme_path(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut provider: ThemeProvider = ThemeProvider::new(theme_path.as_path()).unwrap();
|
||||
// Verify client
|
||||
assert_eq!(provider.theme().auth_address, Color::Yellow);
|
||||
assert_eq!(provider.theme_path, theme_path);
|
||||
assert_eq!(provider.degraded, false);
|
||||
// Mutation
|
||||
provider.theme_mut().auth_address = Color::Green;
|
||||
assert_eq!(provider.theme().auth_address, Color::Green);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_theme_provider_load_and_save() {
|
||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||
let theme_path: PathBuf = get_theme_path(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut provider: ThemeProvider = ThemeProvider::new(theme_path.as_path()).unwrap();
|
||||
// Write
|
||||
provider.theme_mut().auth_address = Color::Green;
|
||||
assert!(provider.save().is_ok());
|
||||
provider.theme_mut().auth_address = Color::Blue;
|
||||
// Reload
|
||||
assert!(provider.load().is_ok());
|
||||
// Unchanged
|
||||
assert_eq!(provider.theme().auth_address, Color::Green);
|
||||
// Instantiate a new provider
|
||||
let provider: ThemeProvider = ThemeProvider::new(theme_path.as_path()).unwrap();
|
||||
assert_eq!(provider.theme().auth_address, Color::Green); // Unchanged
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_theme_provider_degraded() {
|
||||
let mut provider: ThemeProvider = ThemeProvider::degraded();
|
||||
assert_eq!(provider.theme().auth_address, Color::Yellow);
|
||||
assert_eq!(provider.degraded, true);
|
||||
provider.theme_mut().auth_address = Color::Green;
|
||||
assert!(provider.load().is_err());
|
||||
assert_eq!(provider.theme().auth_address, Color::Yellow);
|
||||
assert!(provider.save().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_theme_provider_err() {
|
||||
assert!(ThemeProvider::new(Path::new("/tmp/oifoif/omar")).is_err());
|
||||
}
|
||||
|
||||
/// ### get_theme_path
|
||||
///
|
||||
/// Get paths for theme file
|
||||
fn get_theme_path(dir: &Path) -> PathBuf {
|
||||
let mut p: PathBuf = PathBuf::from(dir);
|
||||
p.push("theme.toml");
|
||||
p
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Dependencies
|
||||
extern crate dirs;
|
||||
|
||||
// Locals
|
||||
use super::{AuthActivity, FileTransferProtocol};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{AuthActivity, FileTransferProtocol};
|
||||
use super::{AuthActivity, FileTransferParams, FileTransferProtocol};
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### protocol_opt_to_enum
|
||||
@@ -80,4 +80,37 @@ impl AuthActivity {
|
||||
self.umount_size_err();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### collect_host_params
|
||||
///
|
||||
/// Get input values from fields or return an error if fields are invalid
|
||||
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let (address, port, protocol, username, password): (
|
||||
String,
|
||||
u16,
|
||||
FileTransferProtocol,
|
||||
String,
|
||||
String,
|
||||
) = self.get_input();
|
||||
if address.is_empty() {
|
||||
return Err("Invalid host");
|
||||
}
|
||||
if port == 0 {
|
||||
return Err("Invalid port");
|
||||
}
|
||||
Ok(FileTransferParams {
|
||||
address,
|
||||
port,
|
||||
protocol,
|
||||
username: match username.is_empty() {
|
||||
true => None,
|
||||
false => Some(username),
|
||||
},
|
||||
password: match password.is_empty() {
|
||||
true => None,
|
||||
false => Some(password),
|
||||
},
|
||||
entry_directory: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,11 @@ mod misc;
|
||||
mod update;
|
||||
mod view;
|
||||
|
||||
// Dependencies
|
||||
extern crate crossterm;
|
||||
extern crate tuirealm;
|
||||
|
||||
// locals
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::ui::context::FileTransferParams;
|
||||
use crate::utils::git;
|
||||
|
||||
// Includes
|
||||
@@ -51,6 +47,7 @@ use tuirealm::{Update, View};
|
||||
const COMPONENT_TEXT_H1: &str = "TEXT_H1";
|
||||
const COMPONENT_TEXT_H2: &str = "TEXT_H2";
|
||||
const COMPONENT_TEXT_NEW_VERSION: &str = "TEXT_NEW_VERSION";
|
||||
const COMPONENT_TEXT_NEW_VERSION_NOTES: &str = "TEXTAREA_NEW_VERSION";
|
||||
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
|
||||
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
||||
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
|
||||
@@ -70,6 +67,7 @@ const COMPONENT_RECENTS_LIST: &str = "RECENTS_LIST";
|
||||
|
||||
// Store keys
|
||||
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
||||
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
|
||||
|
||||
/// ### AuthActivity
|
||||
///
|
||||
@@ -112,44 +110,59 @@ impl AuthActivity {
|
||||
fn check_for_updates(&mut self) {
|
||||
debug!("Check for updates...");
|
||||
// Check version only if unset in the store
|
||||
let ctx: &Context = self.context.as_ref().unwrap();
|
||||
if !ctx.store.isset(STORE_KEY_LATEST_VERSION) {
|
||||
let ctx: &mut Context = self.context_mut();
|
||||
if !ctx.store().isset(STORE_KEY_LATEST_VERSION) {
|
||||
debug!("Version is not set in storage");
|
||||
let mut new_version: Option<String> = match ctx.config_client.as_ref() {
|
||||
Some(client) => {
|
||||
if client.get_check_for_updates() {
|
||||
debug!("Check for updates is enabled");
|
||||
// Send request
|
||||
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
|
||||
Ok(version) => {
|
||||
info!("Latest version is: {:?}", version);
|
||||
version
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
error!("Failed to get latest version: {}", err);
|
||||
self.mount_error(
|
||||
format!("Could not check for new updates: {}", err).as_str(),
|
||||
);
|
||||
// None
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Check for updates is disabled");
|
||||
None
|
||||
if ctx.config().get_check_for_updates() {
|
||||
debug!("Check for updates is enabled");
|
||||
// Send request
|
||||
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
|
||||
Ok(Some(git::GithubTag { tag_name, body })) => {
|
||||
// If some, store version and release notes
|
||||
info!("Latest version is: {}", tag_name);
|
||||
ctx.store_mut()
|
||||
.set_string(STORE_KEY_LATEST_VERSION, tag_name);
|
||||
ctx.store_mut().set_string(STORE_KEY_RELEASE_NOTES, body);
|
||||
}
|
||||
Ok(None) => {
|
||||
info!("Latest version is: {} (current)", env!("CARGO_PKG_VERSION"));
|
||||
// Just set flag as check
|
||||
ctx.store_mut().set(STORE_KEY_LATEST_VERSION);
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
error!("Failed to get latest version: {}", err);
|
||||
self.mount_error(
|
||||
format!("Could not check for new updates: {}", err).as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let ctx: &mut Context = self.context.as_mut().unwrap();
|
||||
// Set version into the store (or just a flag)
|
||||
match new_version.take() {
|
||||
Some(new_version) => ctx.store.set_string(STORE_KEY_LATEST_VERSION, new_version), // If Some, set String
|
||||
None => ctx.store.set(STORE_KEY_LATEST_VERSION), // If None, just set flag
|
||||
} else {
|
||||
info!("Check for updates is disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### context
|
||||
///
|
||||
/// Returns a reference to context
|
||||
fn context(&self) -> &Context {
|
||||
self.context.as_ref().unwrap()
|
||||
}
|
||||
|
||||
/// ### context_mut
|
||||
///
|
||||
/// Returns a mutable reference to context
|
||||
fn context_mut(&mut self) -> &mut Context {
|
||||
self.context.as_mut().unwrap()
|
||||
}
|
||||
|
||||
/// ### theme
|
||||
///
|
||||
/// Returns a reference to theme
|
||||
fn theme(&self) -> &Theme {
|
||||
self.context().theme_provider().theme()
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity for AuthActivity {
|
||||
@@ -161,11 +174,11 @@ impl Activity for AuthActivity {
|
||||
fn on_create(&mut self, mut context: Context) {
|
||||
debug!("Initializing activity");
|
||||
// Initialize file transfer params
|
||||
context.ft_params = Some(FileTransferParams::default());
|
||||
context.set_ftparams(FileTransferParams::default());
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
self.context.as_mut().unwrap().clear_screen();
|
||||
self.context_mut().clear_screen();
|
||||
// Put raw mode on enabled
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
@@ -182,7 +195,7 @@ impl Activity for AuthActivity {
|
||||
self.view_recent_connections();
|
||||
}
|
||||
// Verify error state from context
|
||||
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||
if let Some(err) = self.context_mut().error() {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
info!("Activity initialized");
|
||||
@@ -198,7 +211,7 @@ impl Activity for AuthActivity {
|
||||
return;
|
||||
}
|
||||
// Read one event
|
||||
if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
|
||||
// Set redraw to true
|
||||
self.redraw = true;
|
||||
// Handle on resize
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
*/
|
||||
// locals
|
||||
use super::{
|
||||
AuthActivity, FileTransferParams, FileTransferProtocol, COMPONENT_BOOKMARKS_LIST,
|
||||
COMPONENT_INPUT_ADDR, COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD,
|
||||
COMPONENT_INPUT_PORT, COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||
AuthActivity, FileTransferProtocol, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
|
||||
COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT,
|
||||
COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
|
||||
COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR,
|
||||
COMPONENT_TEXT_HELP, COMPONENT_TEXT_SIZE_ERR,
|
||||
COMPONENT_TEXT_HELP, COMPONENT_TEXT_NEW_VERSION_NOTES, COMPONENT_TEXT_SIZE_ERR,
|
||||
};
|
||||
use crate::ui::keymap::*;
|
||||
use tuirealm::components::InputPropsBuilder;
|
||||
@@ -116,18 +116,6 @@ impl Update for AuthActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
// <TAB> bookmarks
|
||||
(COMPONENT_BOOKMARKS_LIST, &MSG_KEY_TAB)
|
||||
| (COMPONENT_RECENTS_LIST, &MSG_KEY_TAB) => {
|
||||
// Give focus to address
|
||||
self.view.active(COMPONENT_INPUT_ADDR);
|
||||
None
|
||||
}
|
||||
// Any <TAB>, go to bookmarks
|
||||
(_, &MSG_KEY_TAB) => {
|
||||
self.view.active(COMPONENT_BOOKMARKS_LIST);
|
||||
None
|
||||
}
|
||||
// Bookmarks commands
|
||||
// <RIGHT> / <LEFT>
|
||||
(COMPONENT_BOOKMARKS_LIST, &MSG_KEY_RIGHT) => {
|
||||
@@ -219,10 +207,12 @@ impl Update for AuthActivity {
|
||||
self.umount_recent_del_dialog();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, _) => None,
|
||||
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, &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) => {
|
||||
// Umount text error
|
||||
@@ -230,17 +220,31 @@ impl Update for AuthActivity {
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_ERROR, _) => None,
|
||||
(COMPONENT_TEXT_NEW_VERSION_NOTES, &MSG_KEY_ESC)
|
||||
| (COMPONENT_TEXT_NEW_VERSION_NOTES, &MSG_KEY_ENTER) => {
|
||||
// Umount release notes
|
||||
self.umount_release_notes();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_NEW_VERSION_NOTES, _) => None,
|
||||
// Help
|
||||
(_, &MSG_KEY_CTRL_H) => {
|
||||
// Show help
|
||||
self.mount_help();
|
||||
None
|
||||
}
|
||||
// Release notes
|
||||
(_, &MSG_KEY_CTRL_R) => {
|
||||
// Show release notes
|
||||
self.mount_release_notes();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => {
|
||||
// Hide text help
|
||||
self.umount_help();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_HELP, _) => None,
|
||||
// Enter setup
|
||||
(_, &MSG_KEY_CTRL_C) => {
|
||||
self.exit_reason = Some(super::ExitReason::EnterSetup);
|
||||
@@ -293,6 +297,7 @@ impl Update for AuthActivity {
|
||||
self.umount_bookmark_save_dialog();
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_BOOKMARK_NAME, _) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, _) => None,
|
||||
// Quit dialog
|
||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(choice)))) => {
|
||||
// If choice is 0, quit termscp
|
||||
@@ -308,27 +313,34 @@ impl Update for AuthActivity {
|
||||
}
|
||||
// -- text size error; block everything
|
||||
(COMPONENT_TEXT_SIZE_ERR, _) => None,
|
||||
// <TAB> bookmarks
|
||||
(COMPONENT_BOOKMARKS_LIST, &MSG_KEY_TAB)
|
||||
| (COMPONENT_RECENTS_LIST, &MSG_KEY_TAB) => {
|
||||
// Give focus to address
|
||||
self.view.active(COMPONENT_INPUT_ADDR);
|
||||
None
|
||||
}
|
||||
// Any <TAB>, go to bookmarks
|
||||
(_, &MSG_KEY_TAB) => {
|
||||
self.view.active(COMPONENT_BOOKMARKS_LIST);
|
||||
None
|
||||
}
|
||||
// On submit on any unhandled (connect)
|
||||
(_, Msg::OnSubmit(_)) | (_, &MSG_KEY_ENTER) => {
|
||||
// Match <ENTER> key for all other components
|
||||
self.save_recent();
|
||||
let (address, port, protocol, username, password) = self.get_input();
|
||||
// Set file transfer params to context
|
||||
let mut ft_params: &mut FileTransferParams =
|
||||
&mut self.context.as_mut().unwrap().ft_params.as_mut().unwrap();
|
||||
ft_params.address = address;
|
||||
ft_params.port = port;
|
||||
ft_params.protocol = protocol;
|
||||
ft_params.username = match username.is_empty() {
|
||||
true => None,
|
||||
false => Some(username),
|
||||
};
|
||||
ft_params.password = match password.is_empty() {
|
||||
true => None,
|
||||
false => Some(password),
|
||||
};
|
||||
// Set exit reason
|
||||
self.exit_reason = Some(super::ExitReason::Connect);
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ use tuirealm::components::{
|
||||
radio::{Radio, RadioPropsBuilder},
|
||||
scrolltable::{ScrollTablePropsBuilder, Scrolltable},
|
||||
span::{Span, SpanPropsBuilder},
|
||||
textarea::{Textarea, TextareaPropsBuilder},
|
||||
};
|
||||
use tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
@@ -55,6 +56,14 @@ impl AuthActivity {
|
||||
///
|
||||
/// Initialize view, mounting all startup components inside the view
|
||||
pub(super) fn init(&mut self) {
|
||||
let key_color = self.theme().misc_keys;
|
||||
let addr_color = self.theme().auth_address;
|
||||
let protocol_color = self.theme().auth_protocol;
|
||||
let port_color = self.theme().auth_port;
|
||||
let username_color = self.theme().auth_username;
|
||||
let password_color = self.theme().auth_password;
|
||||
let bookmarks_color = self.theme().auth_bookmarks;
|
||||
let recents_color = self.theme().auth_recents;
|
||||
// Headers
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_H1,
|
||||
@@ -85,14 +94,14 @@ impl AuthActivity {
|
||||
TextSpanBuilder::new("Press ").bold().build(),
|
||||
TextSpanBuilder::new("<CTRL+H>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
TextSpanBuilder::new(" to show keybindings; ")
|
||||
.bold()
|
||||
.build(),
|
||||
TextSpanBuilder::new("<CTRL+C>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
TextSpanBuilder::new(" to enter setup").bold().build(),
|
||||
])
|
||||
@@ -100,19 +109,15 @@ impl AuthActivity {
|
||||
)),
|
||||
);
|
||||
// Get default protocol
|
||||
let default_protocol: FileTransferProtocol =
|
||||
match self.context.as_ref().unwrap().config_client.as_ref() {
|
||||
Some(cli) => cli.get_default_protocol(),
|
||||
None => FileTransferProtocol::Sftp,
|
||||
};
|
||||
let default_protocol: FileTransferProtocol = self.context().config().get_default_protocol();
|
||||
// Protocol
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_PROTOCOL,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightGreen)
|
||||
.with_color(protocol_color)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, protocol_color)
|
||||
.with_options(
|
||||
Some(String::from("Protocol")),
|
||||
vec![
|
||||
@@ -131,9 +136,9 @@ impl AuthActivity {
|
||||
super::COMPONENT_INPUT_ADDR,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||
.with_label(String::from("Remote address"))
|
||||
.with_foreground(addr_color)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, addr_color)
|
||||
.with_label(String::from("Remote host"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -142,8 +147,8 @@ impl AuthActivity {
|
||||
super::COMPONENT_INPUT_PORT,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightCyan)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
|
||||
.with_foreground(port_color)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, port_color)
|
||||
.with_label(String::from("Port number"))
|
||||
.with_input(InputType::Number)
|
||||
.with_input_len(5)
|
||||
@@ -156,8 +161,8 @@ impl AuthActivity {
|
||||
super::COMPONENT_INPUT_USERNAME,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightMagenta)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
|
||||
.with_foreground(username_color)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, username_color)
|
||||
.with_label(String::from("Username"))
|
||||
.build(),
|
||||
)),
|
||||
@@ -167,8 +172,8 @@ impl AuthActivity {
|
||||
super::COMPONENT_INPUT_PASSWORD,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
|
||||
.with_foreground(password_color)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, password_color)
|
||||
.with_label(String::from("Password"))
|
||||
.with_input(InputType::Password)
|
||||
.build(),
|
||||
@@ -176,26 +181,23 @@ impl AuthActivity {
|
||||
);
|
||||
// Version notice
|
||||
if let Some(version) = self
|
||||
.context
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.store
|
||||
.context()
|
||||
.store()
|
||||
.get_string(super::STORE_KEY_LATEST_VERSION)
|
||||
{
|
||||
let version: String = version.to_string();
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_NEW_VERSION,
|
||||
Box::new(Span::new(
|
||||
SpanPropsBuilder::default()
|
||||
SpanPropsBuilder::default()
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_spans(
|
||||
vec![
|
||||
TextSpan::from("termscp "),
|
||||
TextSpanBuilder::new(version).underlined().bold().build(),
|
||||
TextSpan::from(" is now available! Download it from <https://github.com/veeso/termscp/releases/latest>")
|
||||
]
|
||||
)
|
||||
.build()
|
||||
))
|
||||
.with_spans(vec![
|
||||
TextSpan::from("termscp "),
|
||||
TextSpanBuilder::new(version.as_str()).underlined().bold().build(),
|
||||
TextSpan::from(" is NOW available! Get it from <https://veeso.github.io/termscp/>; view release notes with <CTRL+R>"),
|
||||
])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
// Bookmarks
|
||||
@@ -203,26 +205,27 @@ impl AuthActivity {
|
||||
super::COMPONENT_BOOKMARKS_LIST,
|
||||
Box::new(BookmarkList::new(
|
||||
BookmarkListPropsBuilder::default()
|
||||
.with_background(Color::LightGreen)
|
||||
.with_background(bookmarks_color)
|
||||
.with_foreground(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, bookmarks_color)
|
||||
.with_bookmarks(Some(String::from("Bookmarks")), vec![])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
let _ = self.view_bookmarks();
|
||||
// Recents
|
||||
self.view.mount(
|
||||
super::COMPONENT_RECENTS_LIST,
|
||||
Box::new(BookmarkList::new(
|
||||
BookmarkListPropsBuilder::default()
|
||||
.with_background(Color::LightBlue)
|
||||
.with_background(recents_color)
|
||||
.with_foreground(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightBlue)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, recents_color)
|
||||
.with_bookmarks(Some(String::from("Recent connections")), vec![])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Load bookmarks
|
||||
let _ = self.view_bookmarks();
|
||||
let _ = self.view_recent_connections();
|
||||
// Active protocol
|
||||
self.view.active(super::COMPONENT_RADIO_PROTOCOL);
|
||||
@@ -233,7 +236,7 @@ impl AuthActivity {
|
||||
/// Display view on canvas
|
||||
pub(super) fn view(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal.draw(|f| {
|
||||
let _ = ctx.terminal().draw(|f| {
|
||||
// Check window size
|
||||
let height: u16 = f.size().height;
|
||||
self.check_minimum_window_size(height);
|
||||
@@ -346,6 +349,15 @@ impl AuthActivity {
|
||||
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_NEW_VERSION_NOTES) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 90, 90);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view
|
||||
.render(super::COMPONENT_TEXT_NEW_VERSION_NOTES, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
@@ -467,12 +479,13 @@ impl AuthActivity {
|
||||
/// Mount error box
|
||||
pub(super) fn mount_error(&mut self, text: &str) {
|
||||
// Mount
|
||||
let err_color = self.theme().misc_error_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_ERROR,
|
||||
Box::new(MsgBox::new(
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.with_borders(Borders::ALL, BorderType::Thick, Color::Red)
|
||||
.with_foreground(err_color)
|
||||
.with_borders(Borders::ALL, BorderType::Thick, err_color)
|
||||
.bold()
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
@@ -494,12 +507,13 @@ impl AuthActivity {
|
||||
/// Mount size error
|
||||
pub(super) fn mount_size_err(&mut self) {
|
||||
// Mount
|
||||
let err_color = self.theme().misc_error_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_SIZE_ERR,
|
||||
Box::new(MsgBox::new(
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.with_borders(Borders::ALL, BorderType::Thick, Color::Red)
|
||||
.with_foreground(err_color)
|
||||
.with_borders(Borders::ALL, BorderType::Thick, err_color)
|
||||
.bold()
|
||||
.with_texts(
|
||||
None,
|
||||
@@ -526,12 +540,13 @@ impl AuthActivity {
|
||||
/// Mount quit popup
|
||||
pub(super) fn mount_quit(&mut self) {
|
||||
// Protocol
|
||||
let quit_color = self.theme().misc_quit_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_QUIT,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Yellow)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||
.with_color(quit_color)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, quit_color)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_options(
|
||||
Some(String::from("Quit termscp?")),
|
||||
@@ -554,13 +569,14 @@ impl AuthActivity {
|
||||
///
|
||||
/// Mount bookmark delete dialog
|
||||
pub(super) fn mount_bookmark_del_dialog(&mut self) {
|
||||
let warn_color = self.theme().misc_warn_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Yellow)
|
||||
.with_color(warn_color)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, warn_color)
|
||||
.with_options(
|
||||
Some(String::from("Delete bookmark?")),
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
@@ -586,13 +602,14 @@ impl AuthActivity {
|
||||
///
|
||||
/// Mount recent delete dialog
|
||||
pub(super) fn mount_recent_del_dialog(&mut self) {
|
||||
let warn_color = self.theme().misc_warn_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Yellow)
|
||||
.with_color(warn_color)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, warn_color)
|
||||
.with_options(
|
||||
Some(String::from("Delete bookmark?")),
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
@@ -616,12 +633,14 @@ impl AuthActivity {
|
||||
///
|
||||
/// Mount bookmark save dialog
|
||||
pub(super) fn mount_bookmark_save_dialog(&mut self) {
|
||||
let save_color = self.theme().misc_save_dialog;
|
||||
let warn_color = self.theme().misc_warn_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_BOOKMARK_NAME,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightCyan)
|
||||
.with_label(String::from("Save bookmark as..."))
|
||||
.with_foreground(save_color)
|
||||
.with_label(String::from("Save bookmark as…"))
|
||||
.with_borders(
|
||||
Borders::TOP | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Rounded,
|
||||
@@ -634,7 +653,7 @@ impl AuthActivity {
|
||||
super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Red)
|
||||
.with_color(warn_color)
|
||||
.with_borders(
|
||||
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Rounded,
|
||||
@@ -663,6 +682,7 @@ impl AuthActivity {
|
||||
///
|
||||
/// Mount help
|
||||
pub(super) fn mount_help(&mut self) {
|
||||
let key_color = self.theme().misc_keys;
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_HELP,
|
||||
Box::new(Scrolltable::new(
|
||||
@@ -677,7 +697,7 @@ impl AuthActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<ESC>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Quit termscp"))
|
||||
@@ -685,7 +705,7 @@ impl AuthActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<TAB>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Switch from form and bookmarks"))
|
||||
@@ -693,7 +713,7 @@ impl AuthActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<RIGHT/LEFT>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Switch bookmark tab"))
|
||||
@@ -701,7 +721,7 @@ impl AuthActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<UP/DOWN>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Move up/down in current tab"))
|
||||
@@ -709,7 +729,7 @@ impl AuthActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<ENTER>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Connect/Load bookmark"))
|
||||
@@ -717,7 +737,7 @@ impl AuthActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<DEL|E>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Delete selected bookmark"))
|
||||
@@ -725,7 +745,7 @@ impl AuthActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<CTRL+C>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Enter setup"))
|
||||
@@ -733,7 +753,7 @@ impl AuthActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<CTRL+S>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Save bookmark"))
|
||||
@@ -753,6 +773,35 @@ impl AuthActivity {
|
||||
self.view.umount(super::COMPONENT_TEXT_HELP);
|
||||
}
|
||||
|
||||
/// ### mount_release_notes
|
||||
///
|
||||
/// mount release notes text area
|
||||
pub(super) fn mount_release_notes(&mut self) {
|
||||
if let Some(ctx) = self.context.as_ref() {
|
||||
if let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES) {
|
||||
// make spans
|
||||
let spans: Vec<TextSpan> = release_notes.lines().map(TextSpan::from).collect();
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_NEW_VERSION_NOTES,
|
||||
Box::new(Textarea::new(
|
||||
TextareaPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||
.with_texts(Some(String::from("Release notes")), spans)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.active(super::COMPONENT_TEXT_NEW_VERSION_NOTES);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### umount_release_notes
|
||||
///
|
||||
/// Umount release notes text area
|
||||
pub(super) fn umount_release_notes(&mut self) {
|
||||
self.view.umount(super::COMPONENT_TEXT_NEW_VERSION_NOTES);
|
||||
}
|
||||
|
||||
/// ### get_input
|
||||
///
|
||||
/// Collect input values from view
|
||||
@@ -774,8 +823,11 @@ impl AuthActivity {
|
||||
|
||||
pub(super) fn get_input_port(&self) -> u16 {
|
||||
match self.view.get_state(super::COMPONENT_INPUT_PORT) {
|
||||
Some(Payload::One(Value::Usize(x))) => x as u16,
|
||||
_ => Self::get_default_port_for_protocol(FileTransferProtocol::Sftp),
|
||||
Some(Payload::One(Value::Usize(x))) => match x > 65535 {
|
||||
true => 0,
|
||||
false => x as u16,
|
||||
},
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
extern crate tempfile;
|
||||
// locals
|
||||
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
|
||||
use crate::filetransfer::FileTransferErrorType;
|
||||
|
||||
@@ -32,7 +32,7 @@ use crate::fs::FsFile;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
impl FileTransferActivity {
|
||||
@@ -48,7 +48,7 @@ impl FileTransferActivity {
|
||||
if entry.is_file() {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Opening file \"{}\"...", entry.get_abs_path().display()),
|
||||
format!("Opening file \"{}\"…", entry.get_abs_path().display()),
|
||||
);
|
||||
// Edit file
|
||||
if let Err(err) = self.edit_local_file(entry.get_abs_path().as_path()) {
|
||||
@@ -72,7 +72,7 @@ impl FileTransferActivity {
|
||||
if let FsEntry::File(file) = entry {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Opening file \"{}\"...", file.abs_path.display()),
|
||||
format!("Opening file \"{}\"…", file.abs_path.display()),
|
||||
);
|
||||
// Edit file
|
||||
if let Err(err) = self.edit_remote_file(file) {
|
||||
@@ -145,44 +145,42 @@ impl FileTransferActivity {
|
||||
/// Edit file on remote host
|
||||
fn edit_remote_file(&mut self, file: FsFile) -> Result<(), String> {
|
||||
// Create temp file
|
||||
let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
return Err(format!("Could not create temporary file: {}", err));
|
||||
}
|
||||
let tmpfile: PathBuf = match self.download_file_as_temp(&file) {
|
||||
Ok(p) => p,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
// Download file
|
||||
let file_name = file.name.clone();
|
||||
let file_path = file.abs_path.clone();
|
||||
if let Err(err) = self.filetransfer_recv(
|
||||
TransferPayload::File(file),
|
||||
tmpfile.path(),
|
||||
tmpfile.as_path(),
|
||||
Some(file_name.clone()),
|
||||
) {
|
||||
return Err(format!("Could not open file {}: {}", file_name, err));
|
||||
}
|
||||
// Get current file modification time
|
||||
let prev_mtime: SystemTime = match self.host.stat(tmpfile.path()) {
|
||||
let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) {
|
||||
Ok(e) => e.get_last_change_time(),
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.path().display(),
|
||||
tmpfile.as_path().display(),
|
||||
err
|
||||
))
|
||||
}
|
||||
};
|
||||
// Edit file
|
||||
if let Err(err) = self.edit_local_file(tmpfile.path()) {
|
||||
if let Err(err) = self.edit_local_file(tmpfile.as_path()) {
|
||||
return Err(err);
|
||||
}
|
||||
// Get local fs entry
|
||||
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.path()) {
|
||||
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.as_path()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.path().display(),
|
||||
tmpfile.as_path().display(),
|
||||
err
|
||||
))
|
||||
}
|
||||
@@ -198,12 +196,12 @@ impl FileTransferActivity {
|
||||
),
|
||||
);
|
||||
// Get local fs entry
|
||||
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) {
|
||||
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.as_path()) {
|
||||
Ok(e) => e.unwrap_file(),
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.path().display(),
|
||||
tmpfile.as_path().display(),
|
||||
err
|
||||
))
|
||||
}
|
||||
|
||||
@@ -173,4 +173,49 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_find_open(&mut self) {
|
||||
match self.get_found_selected_entries() {
|
||||
SelectedEntry::One(entry) => {
|
||||
// Open file
|
||||
self.open_found_file(&entry, None);
|
||||
}
|
||||
SelectedEntry::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
// Open file
|
||||
self.open_found_file(entry, None);
|
||||
}
|
||||
}
|
||||
SelectedEntry::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_find_open_with(&mut self, with: &str) {
|
||||
match self.get_found_selected_entries() {
|
||||
SelectedEntry::One(entry) => {
|
||||
// Open file
|
||||
self.open_found_file(&entry, Some(with));
|
||||
}
|
||||
SelectedEntry::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
// Open file
|
||||
self.open_found_file(entry, Some(with));
|
||||
}
|
||||
}
|
||||
SelectedEntry::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_found_file(&mut self, entry: &FsEntry, with: Option<&str>) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||
self.action_open_local_file(entry, with);
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
self.action_open_remote_file(entry, with);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,15 +37,19 @@ pub(crate) mod exec;
|
||||
pub(crate) mod find;
|
||||
pub(crate) mod mkdir;
|
||||
pub(crate) mod newfile;
|
||||
pub(crate) mod open;
|
||||
pub(crate) mod rename;
|
||||
pub(crate) mod save;
|
||||
pub(crate) mod submit;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SelectedEntry {
|
||||
One(FsEntry),
|
||||
Many(Vec<FsEntry>),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SelectedEntryIndex {
|
||||
One(usize),
|
||||
Many(Vec<usize>),
|
||||
|
||||
166
src/ui/activities/filetransfer/actions/open.rs
Normal file
166
src/ui/activities/filetransfer/actions/open.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
/**
|
||||
* 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 super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
|
||||
// ext
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### action_open_local
|
||||
///
|
||||
/// Open local file
|
||||
pub(crate) fn action_open_local(&mut self) {
|
||||
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
|
||||
SelectedEntry::One(entry) => vec![entry],
|
||||
SelectedEntry::Many(entries) => entries,
|
||||
SelectedEntry::None => vec![],
|
||||
};
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_local_file(x, None));
|
||||
}
|
||||
|
||||
/// ### action_open_remote
|
||||
///
|
||||
/// Open local file
|
||||
pub(crate) fn action_open_remote(&mut self) {
|
||||
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
|
||||
SelectedEntry::One(entry) => vec![entry],
|
||||
SelectedEntry::Many(entries) => entries,
|
||||
SelectedEntry::None => vec![],
|
||||
};
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_remote_file(x, None));
|
||||
}
|
||||
|
||||
/// ### action_open_local_file
|
||||
///
|
||||
/// Perform open lopcal file
|
||||
pub(crate) fn action_open_local_file(&mut self, entry: &FsEntry, open_with: Option<&str>) {
|
||||
let entry: FsEntry = entry.get_realfile();
|
||||
self.open_path_with(entry.get_abs_path().as_path(), open_with);
|
||||
}
|
||||
|
||||
/// ### action_open_local
|
||||
///
|
||||
/// Open remote file. The file is first downloaded to a temporary directory on localhost
|
||||
pub(crate) fn action_open_remote_file(&mut self, entry: &FsEntry, open_with: Option<&str>) {
|
||||
let entry: FsEntry = entry.get_realfile();
|
||||
// Download file
|
||||
let tmpfile: String =
|
||||
match self.get_cache_tmp_name(entry.get_name(), entry.get_ftype().as_deref()) {
|
||||
None => {
|
||||
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
||||
return;
|
||||
}
|
||||
Some(p) => p,
|
||||
};
|
||||
let cache: PathBuf = match self.cache.as_ref() {
|
||||
None => {
|
||||
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
||||
return;
|
||||
}
|
||||
Some(p) => p.path().to_path_buf(),
|
||||
};
|
||||
match self.filetransfer_recv(
|
||||
TransferPayload::Any(entry),
|
||||
cache.as_path(),
|
||||
Some(tmpfile.clone()),
|
||||
) {
|
||||
Ok(_) => {
|
||||
// Make file and open if file exists
|
||||
let mut tmp: PathBuf = cache;
|
||||
tmp.push(tmpfile.as_str());
|
||||
if tmp.exists() {
|
||||
self.open_path_with(tmp.as_path(), open_with);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!("Failed to download remote entry: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### action_local_open_with
|
||||
///
|
||||
/// Open selected file with provided application
|
||||
pub(crate) fn action_local_open_with(&mut self, with: &str) {
|
||||
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
|
||||
SelectedEntry::One(entry) => vec![entry],
|
||||
SelectedEntry::Many(entries) => entries,
|
||||
SelectedEntry::None => vec![],
|
||||
};
|
||||
// Open all entries
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_local_file(x, Some(with)));
|
||||
}
|
||||
|
||||
/// ### action_remote_open_with
|
||||
///
|
||||
/// Open selected file with provided application
|
||||
pub(crate) fn action_remote_open_with(&mut self, with: &str) {
|
||||
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
|
||||
SelectedEntry::One(entry) => vec![entry],
|
||||
SelectedEntry::Many(entries) => entries,
|
||||
SelectedEntry::None => vec![],
|
||||
};
|
||||
// Open all entries
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_remote_file(x, Some(with)));
|
||||
}
|
||||
|
||||
/// ### open_path_with
|
||||
///
|
||||
/// Common function which opens a path with default or specified program.
|
||||
fn open_path_with(&mut self, p: &Path, with: Option<&str>) {
|
||||
// Open file
|
||||
let result = match with {
|
||||
None => open::that(p),
|
||||
Some(with) => open::with(p, with),
|
||||
};
|
||||
// Log result
|
||||
match result {
|
||||
Ok(_) => self.log(LogLevel::Info, format!("Opened file `{}`", p.display())),
|
||||
Err(err) => self.log(
|
||||
LogLevel::Error,
|
||||
format!("Failed to open filoe `{}`: {}", p.display(), err),
|
||||
),
|
||||
}
|
||||
// NOTE: clear screen in order to prevent crap on stderr
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/ui/activities/filetransfer/actions/submit.rs
Normal file
88
src/ui/activities/filetransfer/actions/submit.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
/**
|
||||
* 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 super::{FileTransferActivity, FsEntry};
|
||||
|
||||
enum SubmitAction {
|
||||
ChangeDir,
|
||||
None,
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### action_submit_local
|
||||
///
|
||||
/// Decides which action to perform on submit for local explorer
|
||||
/// Return true whether the directory changed
|
||||
pub(crate) fn action_submit_local(&mut self, entry: FsEntry) -> bool {
|
||||
let action: SubmitAction = match &entry {
|
||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
||||
FsEntry::File(file) => {
|
||||
match &file.symlink {
|
||||
Some(symlink_entry) => {
|
||||
// If symlink and is directory, point to symlink
|
||||
match &**symlink_entry {
|
||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
||||
_ => SubmitAction::None,
|
||||
}
|
||||
}
|
||||
None => SubmitAction::None,
|
||||
}
|
||||
}
|
||||
};
|
||||
match action {
|
||||
SubmitAction::ChangeDir => self.action_enter_local_dir(entry, false),
|
||||
SubmitAction::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### action_submit_remote
|
||||
///
|
||||
/// Decides which action to perform on submit for remote explorer
|
||||
/// Return true whether the directory changed
|
||||
pub(crate) fn action_submit_remote(&mut self, entry: FsEntry) -> bool {
|
||||
let action: SubmitAction = match &entry {
|
||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
||||
FsEntry::File(file) => {
|
||||
match &file.symlink {
|
||||
Some(symlink_entry) => {
|
||||
// If symlink and is directory, point to symlink
|
||||
match &**symlink_entry {
|
||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
||||
_ => SubmitAction::None,
|
||||
}
|
||||
}
|
||||
None => SubmitAction::None,
|
||||
}
|
||||
}
|
||||
};
|
||||
match action {
|
||||
SubmitAction::ChangeDir => self.action_enter_remote_dir(entry, false),
|
||||
SubmitAction::None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ impl Browser {
|
||||
/// ### new
|
||||
///
|
||||
/// Build a new `Browser` struct
|
||||
pub fn new(cli: Option<&ConfigClient>) -> Self {
|
||||
pub fn new(cli: &ConfigClient) -> Self {
|
||||
Self {
|
||||
local: Self::build_local_explorer(cli),
|
||||
remote: Self::build_remote_explorer(cli),
|
||||
@@ -120,45 +120,32 @@ impl Browser {
|
||||
/// ### build_local_explorer
|
||||
///
|
||||
/// Build a file explorer with local host setup
|
||||
pub fn build_local_explorer(cli: Option<&ConfigClient>) -> FileExplorer {
|
||||
pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer {
|
||||
let mut builder = Self::build_explorer(cli);
|
||||
if let Some(cli) = cli {
|
||||
builder.with_formatter(cli.get_local_file_fmt().as_deref());
|
||||
}
|
||||
builder.with_formatter(cli.get_local_file_fmt().as_deref());
|
||||
builder.build()
|
||||
}
|
||||
|
||||
/// ### build_remote_explorer
|
||||
///
|
||||
/// Build a file explorer with remote host setup
|
||||
pub fn build_remote_explorer(cli: Option<&ConfigClient>) -> FileExplorer {
|
||||
pub fn build_remote_explorer(cli: &ConfigClient) -> FileExplorer {
|
||||
let mut builder = Self::build_explorer(cli);
|
||||
if let Some(cli) = cli {
|
||||
builder.with_formatter(cli.get_remote_file_fmt().as_deref());
|
||||
}
|
||||
builder.with_formatter(cli.get_remote_file_fmt().as_deref());
|
||||
builder.build()
|
||||
}
|
||||
|
||||
/// ### build_explorer
|
||||
///
|
||||
/// Build explorer reading configuration from `ConfigClient`
|
||||
fn build_explorer(cli: Option<&ConfigClient>) -> FileExplorerBuilder {
|
||||
fn build_explorer(cli: &ConfigClient) -> FileExplorerBuilder {
|
||||
let mut builder: FileExplorerBuilder = FileExplorerBuilder::new();
|
||||
// Set common keys
|
||||
builder
|
||||
.with_file_sorting(FileSorting::ByName)
|
||||
.with_stack_size(16);
|
||||
match &cli {
|
||||
Some(cli) => {
|
||||
builder // Build according to current configuration
|
||||
.with_group_dirs(cli.get_group_dirs())
|
||||
.with_hidden_files(cli.get_show_hidden_files());
|
||||
}
|
||||
None => {
|
||||
builder // Build default
|
||||
.with_group_dirs(Some(GroupDirs::First));
|
||||
}
|
||||
};
|
||||
.with_stack_size(16)
|
||||
.with_group_dirs(cli.get_group_dirs())
|
||||
.with_hidden_files(cli.get_show_hidden_files());
|
||||
builder
|
||||
}
|
||||
|
||||
@@ -171,7 +158,7 @@ impl Browser {
|
||||
.with_group_dirs(Some(GroupDirs::First))
|
||||
.with_hidden_files(true)
|
||||
.with_stack_size(0)
|
||||
.with_formatter(Some("{NAME} {SYMLINK}"))
|
||||
.with_formatter(Some("{NAME:32} {SYMLINK}"))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Initialize configuration client if possible.
|
||||
/// This function doesn't return errors.
|
||||
pub(super) fn init_config_client() -> Option<ConfigClient> {
|
||||
pub(super) fn init_config_client() -> ConfigClient {
|
||||
match environment::init_config_dir() {
|
||||
Ok(termscp_dir) => match termscp_dir {
|
||||
Some(termscp_dir) => {
|
||||
@@ -79,34 +79,28 @@ impl FileTransferActivity {
|
||||
let (config_path, ssh_keys_path): (PathBuf, PathBuf) =
|
||||
environment::get_config_paths(termscp_dir.as_path());
|
||||
match ConfigClient::new(config_path.as_path(), ssh_keys_path.as_path()) {
|
||||
Ok(config_client) => Some(config_client),
|
||||
Err(_) => None,
|
||||
Ok(config_client) => config_client,
|
||||
Err(_) => ConfigClient::degraded(),
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
None => ConfigClient::degraded(),
|
||||
},
|
||||
Err(_) => None,
|
||||
Err(_) => ConfigClient::degraded(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### make_ssh_storage
|
||||
///
|
||||
/// Make ssh storage from `ConfigClient` if possible, empty otherwise
|
||||
pub(super) fn make_ssh_storage(cli: Option<&ConfigClient>) -> SshKeyStorage {
|
||||
match cli {
|
||||
Some(cli) => SshKeyStorage::storage_from_config(cli),
|
||||
None => SshKeyStorage::empty(),
|
||||
}
|
||||
/// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded)
|
||||
pub(super) fn make_ssh_storage(cli: &ConfigClient) -> SshKeyStorage {
|
||||
SshKeyStorage::storage_from_config(cli)
|
||||
}
|
||||
|
||||
/// ### setup_text_editor
|
||||
///
|
||||
/// Set text editor to use
|
||||
pub(super) fn setup_text_editor(&self) {
|
||||
if let Some(config_cli) = self.context.as_ref().unwrap().config_client.as_ref() {
|
||||
// Set text editor
|
||||
env::set_var("EDITOR", config_cli.get_text_editor());
|
||||
}
|
||||
env::set_var("EDITOR", self.config().get_text_editor());
|
||||
}
|
||||
|
||||
/// ### read_input_event
|
||||
@@ -114,7 +108,7 @@ impl FileTransferActivity {
|
||||
/// Read one event.
|
||||
/// Returns whether at least one event has been handled
|
||||
pub(super) fn read_input_event(&mut self) -> bool {
|
||||
if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
|
||||
// Handle event
|
||||
let msg = self.view.on(event);
|
||||
self.update(msg);
|
||||
|
||||
@@ -33,14 +33,9 @@ pub(self) mod session;
|
||||
pub(self) mod update;
|
||||
pub(self) mod view;
|
||||
|
||||
// Dependencies
|
||||
extern crate chrono;
|
||||
extern crate crossterm;
|
||||
extern crate textwrap;
|
||||
extern crate tuirealm;
|
||||
|
||||
// locals
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::ftp_transfer::FtpFileTransfer;
|
||||
use crate::filetransfer::scp_transfer::ScpFileTransfer;
|
||||
use crate::filetransfer::sftp_transfer::SftpFileTransfer;
|
||||
@@ -58,7 +53,7 @@ pub(self) use session::TransferPayload;
|
||||
use chrono::{DateTime, Local};
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
use tuirealm::View;
|
||||
|
||||
// -- Storage keys
|
||||
@@ -83,6 +78,7 @@ const COMPONENT_INPUT_FIND: &str = "INPUT_FIND";
|
||||
const COMPONENT_INPUT_GOTO: &str = "INPUT_GOTO";
|
||||
const COMPONENT_INPUT_MKDIR: &str = "INPUT_MKDIR";
|
||||
const COMPONENT_INPUT_NEWFILE: &str = "INPUT_NEWFILE";
|
||||
const COMPONENT_INPUT_OPEN_WITH: &str = "INPUT_OPEN_WITH";
|
||||
const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME";
|
||||
const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS";
|
||||
const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
|
||||
@@ -136,6 +132,7 @@ pub struct FileTransferActivity {
|
||||
browser: Browser, // Browser
|
||||
log_records: VecDeque<LogRecord>, // Log records
|
||||
transfer: TransferStates, // Transfer states
|
||||
cache: Option<TempDir>, // Temporary directory where to store stuff
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
@@ -144,7 +141,7 @@ impl FileTransferActivity {
|
||||
/// Instantiates a new FileTransferActivity
|
||||
pub fn new(host: Localhost, protocol: FileTransferProtocol) -> FileTransferActivity {
|
||||
// Get config client
|
||||
let config_client: Option<ConfigClient> = Self::init_config_client();
|
||||
let config_client: ConfigClient = Self::init_config_client();
|
||||
FileTransferActivity {
|
||||
exit_reason: None,
|
||||
context: None,
|
||||
@@ -152,42 +149,94 @@ impl FileTransferActivity {
|
||||
host,
|
||||
client: match protocol {
|
||||
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
|
||||
Self::make_ssh_storage(config_client.as_ref()),
|
||||
Self::make_ssh_storage(&config_client),
|
||||
)),
|
||||
FileTransferProtocol::Ftp(ftps) => Box::new(FtpFileTransfer::new(ftps)),
|
||||
FileTransferProtocol::Scp => Box::new(ScpFileTransfer::new(
|
||||
Self::make_ssh_storage(config_client.as_ref()),
|
||||
)),
|
||||
FileTransferProtocol::Scp => {
|
||||
Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client)))
|
||||
}
|
||||
},
|
||||
browser: Browser::new(config_client.as_ref()),
|
||||
browser: Browser::new(&config_client),
|
||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||
transfer: TransferStates::default(),
|
||||
cache: match TempDir::new() {
|
||||
Ok(d) => Some(d),
|
||||
Err(_) => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn local(&self) -> &FileExplorer {
|
||||
fn local(&self) -> &FileExplorer {
|
||||
self.browser.local()
|
||||
}
|
||||
|
||||
pub(crate) fn local_mut(&mut self) -> &mut FileExplorer {
|
||||
fn local_mut(&mut self) -> &mut FileExplorer {
|
||||
self.browser.local_mut()
|
||||
}
|
||||
|
||||
pub(crate) fn remote(&self) -> &FileExplorer {
|
||||
fn remote(&self) -> &FileExplorer {
|
||||
self.browser.remote()
|
||||
}
|
||||
|
||||
pub(crate) fn remote_mut(&mut self) -> &mut FileExplorer {
|
||||
fn remote_mut(&mut self) -> &mut FileExplorer {
|
||||
self.browser.remote_mut()
|
||||
}
|
||||
|
||||
pub(crate) fn found(&self) -> Option<&FileExplorer> {
|
||||
fn found(&self) -> Option<&FileExplorer> {
|
||||
self.browser.found()
|
||||
}
|
||||
|
||||
pub(crate) fn found_mut(&mut self) -> Option<&mut FileExplorer> {
|
||||
fn found_mut(&mut self) -> Option<&mut FileExplorer> {
|
||||
self.browser.found_mut()
|
||||
}
|
||||
|
||||
/// ### get_cache_tmp_name
|
||||
///
|
||||
/// Get file name for a file in cache
|
||||
fn get_cache_tmp_name(&self, name: &str, file_type: Option<&str>) -> Option<String> {
|
||||
self.cache.as_ref().map(|_| {
|
||||
let base: String = format!(
|
||||
"{}-{}",
|
||||
name,
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis()
|
||||
);
|
||||
match file_type {
|
||||
None => base,
|
||||
Some(file_type) => format!("{}.{}", base, file_type),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// ### context
|
||||
///
|
||||
/// Returns a reference to context
|
||||
fn context(&self) -> &Context {
|
||||
self.context.as_ref().unwrap()
|
||||
}
|
||||
|
||||
/// ### context_mut
|
||||
///
|
||||
/// Returns a mutable reference to context
|
||||
fn context_mut(&mut self) -> &mut Context {
|
||||
self.context.as_mut().unwrap()
|
||||
}
|
||||
|
||||
/// ### config
|
||||
///
|
||||
/// Returns config client reference
|
||||
fn config(&self) -> &ConfigClient {
|
||||
&self.context().config()
|
||||
}
|
||||
|
||||
/// ### theme
|
||||
///
|
||||
/// Get a reference to `Theme`
|
||||
fn theme(&self) -> &Theme {
|
||||
self.context().theme_provider().theme()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,16 +255,13 @@ impl Activity for FileTransferActivity {
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
self.context.as_mut().unwrap().clear_screen();
|
||||
self.context_mut().clear_screen();
|
||||
// Put raw mode on enabled
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Set working directory
|
||||
let pwd: PathBuf = self.host.pwd();
|
||||
// Get files at current wd
|
||||
self.local_scan(pwd.as_path());
|
||||
self.local_mut().wrkdir = pwd;
|
||||
// Get files at current pwd
|
||||
self.reload_local_dir();
|
||||
debug!("Read working directory");
|
||||
// Configure text editor
|
||||
self.setup_text_editor();
|
||||
@@ -224,7 +270,7 @@ impl Activity for FileTransferActivity {
|
||||
self.init();
|
||||
debug!("Initialized view");
|
||||
// Verify error state from context
|
||||
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||
if let Some(err) = self.context.as_mut().unwrap().error() {
|
||||
error!("Fatal error on create: {}", err);
|
||||
self.mount_fatal(&err);
|
||||
}
|
||||
@@ -244,12 +290,12 @@ impl Activity for FileTransferActivity {
|
||||
}
|
||||
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
|
||||
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
|
||||
let params = self.context().ft_params().unwrap();
|
||||
info!(
|
||||
"Client is not connected to remote; connecting to {}:{}",
|
||||
params.address, params.port
|
||||
);
|
||||
let msg: String = format!("Connecting to {}:{}...", params.address, params.port);
|
||||
let msg: String = format!("Connecting to {}:{}…", params.address, params.port);
|
||||
// Set init state to connecting popup
|
||||
self.mount_wait(msg.as_str());
|
||||
// Force ui draw
|
||||
@@ -281,6 +327,12 @@ impl Activity for FileTransferActivity {
|
||||
/// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity.
|
||||
/// This function must be called once before terminating the activity.
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Destroy cache
|
||||
if let Some(cache) = self.cache.take() {
|
||||
if let Err(err) = cache.close() {
|
||||
error!("Failed to delete cache: {}", err);
|
||||
}
|
||||
}
|
||||
// Disable raw mode
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
|
||||
@@ -25,12 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
extern crate bytesize;
|
||||
extern crate content_inspector;
|
||||
extern crate crossterm;
|
||||
extern crate tempfile;
|
||||
|
||||
// Locals
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
use crate::filetransfer::FileTransferError;
|
||||
@@ -82,15 +76,15 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Connect to remote
|
||||
pub(super) fn connect(&mut self) {
|
||||
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
|
||||
let params = self.context().ft_params().unwrap().clone();
|
||||
let addr: String = params.address.clone();
|
||||
let entry_dir: Option<PathBuf> = params.entry_directory.clone();
|
||||
// Connect to remote
|
||||
match self.client.connect(
|
||||
params.address.clone(),
|
||||
params.address,
|
||||
params.port,
|
||||
params.username.clone(),
|
||||
params.password.clone(),
|
||||
params.username,
|
||||
params.password,
|
||||
) {
|
||||
Ok(welcome) => {
|
||||
if let Some(banner) = welcome {
|
||||
@@ -127,8 +121,8 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// disconnect from remote
|
||||
pub(super) fn disconnect(&mut self) {
|
||||
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
|
||||
let msg: String = format!("Disconnecting from {}...", params.address);
|
||||
let params = self.context().ft_params().unwrap();
|
||||
let msg: String = format!("Disconnecting from {}…", params.address);
|
||||
// Show popup disconnecting
|
||||
self.mount_wait(msg.as_str());
|
||||
// Disconnect
|
||||
@@ -147,19 +141,59 @@ impl FileTransferActivity {
|
||||
|
||||
/// ### reload_remote_dir
|
||||
///
|
||||
/// Reload remote directory entries
|
||||
/// Reload remote directory entries and update browser
|
||||
pub(super) fn reload_remote_dir(&mut self) {
|
||||
// Get current entries
|
||||
if let Ok(pwd) = self.client.pwd() {
|
||||
self.remote_scan(pwd.as_path());
|
||||
if let Ok(wrkdir) = self.client.pwd() {
|
||||
self.remote_scan(wrkdir.as_path());
|
||||
// Set wrkdir
|
||||
self.remote_mut().wrkdir = pwd;
|
||||
self.remote_mut().wrkdir = wrkdir;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### reload_local_dir
|
||||
///
|
||||
/// Reload local directory entries and update browser
|
||||
pub(super) fn reload_local_dir(&mut self) {
|
||||
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
||||
let wrkdir: PathBuf = self.host.pwd();
|
||||
self.local_scan(wrkdir.as_path());
|
||||
self.local_mut().wrkdir = wrkdir;
|
||||
}
|
||||
|
||||
/// ### local_scan
|
||||
///
|
||||
/// Scan current local directory
|
||||
fn local_scan(&mut self, path: &Path) {
|
||||
match self.host.scan_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.local_mut().set_files(files);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current directory: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### remote_scan
|
||||
///
|
||||
/// Scan current remote directory
|
||||
fn remote_scan(&mut self, path: &Path) {
|
||||
match self.client.list_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.remote_mut().set_files(files);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current directory: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### filetransfer_send
|
||||
@@ -202,7 +236,7 @@ impl FileTransferActivity {
|
||||
let total_transfer_size: usize = file.size;
|
||||
self.transfer.full.init(total_transfer_size);
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Uploading {}...", file.abs_path.display()));
|
||||
self.mount_progress_bar(format!("Uploading {}…", file.abs_path.display()));
|
||||
// Get remote path
|
||||
let file_name: String = file.name.clone();
|
||||
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
|
||||
@@ -234,7 +268,7 @@ impl FileTransferActivity {
|
||||
let total_transfer_size: usize = self.get_total_transfer_size_local(entry);
|
||||
self.transfer.full.init(total_transfer_size);
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Uploading {}...", entry.get_abs_path().display()));
|
||||
self.mount_progress_bar(format!("Uploading {}…", entry.get_abs_path().display()));
|
||||
// Send recurse
|
||||
self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
|
||||
// Umount progress bar
|
||||
@@ -259,7 +293,7 @@ impl FileTransferActivity {
|
||||
.sum();
|
||||
self.transfer.full.init(total_transfer_size);
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Uploading {} entries...", entries.len()));
|
||||
self.mount_progress_bar(format!("Uploading {} entries…", entries.len()));
|
||||
// Send recurse
|
||||
entries
|
||||
.iter()
|
||||
@@ -416,16 +450,22 @@ impl FileTransferActivity {
|
||||
// Write remote file
|
||||
let mut total_bytes_written: usize = 0;
|
||||
let mut last_progress_val: f64 = 0.0;
|
||||
let mut last_input_event_fetch: Instant = Instant::now();
|
||||
let mut last_input_event_fetch: Option<Instant> = None;
|
||||
// While the entire file hasn't been completely written,
|
||||
// Or filetransfer has been aborted
|
||||
while total_bytes_written < file_size && !self.transfer.aborted() {
|
||||
// Handle input events (each 500ms)
|
||||
if last_input_event_fetch.elapsed().as_millis() >= 500 {
|
||||
// Handle input events (each 500ms) or if never fetched before
|
||||
if last_input_event_fetch.is_none()
|
||||
|| last_input_event_fetch
|
||||
.unwrap_or_else(Instant::now)
|
||||
.elapsed()
|
||||
.as_millis()
|
||||
>= 500
|
||||
{
|
||||
// Read events
|
||||
self.read_input_event();
|
||||
// Reset instant
|
||||
last_input_event_fetch = Instant::now();
|
||||
last_input_event_fetch = Some(Instant::now());
|
||||
}
|
||||
// Read till you can
|
||||
let mut buffer: [u8; 65536] = [0; 65536];
|
||||
@@ -462,7 +502,7 @@ impl FileTransferActivity {
|
||||
// Draw only if a significant progress has been made (performance improvement)
|
||||
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
|
||||
// Draw
|
||||
self.update_progress_bar(format!("Uploading \"{}\"...", file_name));
|
||||
self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
|
||||
self.view();
|
||||
last_progress_val = self.transfer.partial.calc_progress();
|
||||
}
|
||||
@@ -531,7 +571,7 @@ impl FileTransferActivity {
|
||||
let total_transfer_size: usize = self.get_total_transfer_size_remote(entry);
|
||||
self.transfer.full.init(total_transfer_size);
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Downloading {}...", entry.get_abs_path().display()));
|
||||
self.mount_progress_bar(format!("Downloading {}…", entry.get_abs_path().display()));
|
||||
// Receive
|
||||
self.filetransfer_recv_recurse(entry, local_path, dst_name);
|
||||
// Umount progress bar
|
||||
@@ -549,7 +589,7 @@ impl FileTransferActivity {
|
||||
let total_transfer_size: usize = entry.size;
|
||||
self.transfer.full.init(total_transfer_size);
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Downloading {}...", entry.abs_path.display()));
|
||||
self.mount_progress_bar(format!("Downloading {}…", entry.abs_path.display()));
|
||||
// Receive
|
||||
let result = self.filetransfer_recv_one(local_path, entry, entry.name.clone());
|
||||
// Umount progress bar
|
||||
@@ -575,7 +615,7 @@ impl FileTransferActivity {
|
||||
.sum();
|
||||
self.transfer.full.init(total_transfer_size);
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Downloading {} entries...", entries.len()));
|
||||
self.mount_progress_bar(format!("Downloading {} entries…", entries.len()));
|
||||
// Send recurse
|
||||
entries
|
||||
.iter()
|
||||
@@ -722,7 +762,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
// Reload directory on local
|
||||
self.local_scan(local_path);
|
||||
self.reload_local_dir();
|
||||
// if aborted; show alert
|
||||
if self.transfer.aborted() {
|
||||
// Log abort
|
||||
@@ -756,16 +796,22 @@ impl FileTransferActivity {
|
||||
self.transfer.partial.init(remote.size);
|
||||
// Write local file
|
||||
let mut last_progress_val: f64 = 0.0;
|
||||
let mut last_input_event_fetch: Instant = Instant::now();
|
||||
let mut last_input_event_fetch: Option<Instant> = None;
|
||||
// While the entire file hasn't been completely read,
|
||||
// Or filetransfer has been aborted
|
||||
while total_bytes_written < remote.size && !self.transfer.aborted() {
|
||||
// Handle input events (each 500 ms)
|
||||
if last_input_event_fetch.elapsed().as_millis() >= 500 {
|
||||
// Handle input events (each 500 ms) or is None
|
||||
if last_input_event_fetch.is_none()
|
||||
|| last_input_event_fetch
|
||||
.unwrap_or_else(Instant::now)
|
||||
.elapsed()
|
||||
.as_millis()
|
||||
>= 500
|
||||
{
|
||||
// Read events
|
||||
self.read_input_event();
|
||||
// Reset instant
|
||||
last_input_event_fetch = Instant::now();
|
||||
last_input_event_fetch = Some(Instant::now());
|
||||
}
|
||||
// Read till you can
|
||||
let mut buffer: [u8; 65536] = [0; 65536];
|
||||
@@ -855,42 +901,6 @@ impl FileTransferActivity {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### local_scan
|
||||
///
|
||||
/// Scan current local directory
|
||||
pub(super) fn local_scan(&mut self, path: &Path) {
|
||||
match self.host.scan_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.local_mut().set_files(files);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current directory: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### remote_scan
|
||||
///
|
||||
/// Scan current remote directory
|
||||
pub(super) fn remote_scan(&mut self, path: &Path) {
|
||||
match self.client.list_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.remote_mut().set_files(files);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current directory: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### local_changedir
|
||||
///
|
||||
/// Change directory for local
|
||||
@@ -905,9 +915,7 @@ impl FileTransferActivity {
|
||||
format!("Changed directory on local: {}", path.display()),
|
||||
);
|
||||
// Reload files
|
||||
self.local_scan(path);
|
||||
// Set wrkdir
|
||||
self.local_mut().wrkdir = PathBuf::from(path);
|
||||
self.reload_local_dir();
|
||||
// Push prev_dir to stack
|
||||
if push {
|
||||
self.local_mut().pushd(prev_dir.as_path())
|
||||
@@ -934,9 +942,7 @@ impl FileTransferActivity {
|
||||
format!("Changed directory on remote: {}", path.display()),
|
||||
);
|
||||
// Update files
|
||||
self.remote_scan(path);
|
||||
// Set wrkdir
|
||||
self.remote_mut().wrkdir = PathBuf::from(path);
|
||||
self.reload_remote_dir();
|
||||
// Push prev_dir to stack
|
||||
if push {
|
||||
self.remote_mut().pushd(prev_dir.as_path())
|
||||
@@ -952,6 +958,37 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### download_file_as_temp
|
||||
///
|
||||
/// Download provided file as a temporary file
|
||||
pub(super) fn download_file_as_temp(&mut self, file: &FsFile) -> Result<PathBuf, String> {
|
||||
let tmpfile: PathBuf = match self.cache.as_ref() {
|
||||
Some(cache) => {
|
||||
let mut p: PathBuf = cache.path().to_path_buf();
|
||||
p.push(file.name.as_str());
|
||||
p
|
||||
}
|
||||
None => {
|
||||
return Err(String::from(
|
||||
"Could not create tempfile: cache not available",
|
||||
))
|
||||
}
|
||||
};
|
||||
// Download file
|
||||
match self.filetransfer_recv(
|
||||
TransferPayload::File(file.clone()),
|
||||
tmpfile.as_path(),
|
||||
Some(file.name.clone()),
|
||||
) {
|
||||
Err(err) => Err(format!(
|
||||
"Could not download {} to temporary file: {}",
|
||||
file.abs_path.display(),
|
||||
err
|
||||
)),
|
||||
Ok(()) => Ok(tmpfile),
|
||||
}
|
||||
}
|
||||
|
||||
// -- transfer sizes
|
||||
|
||||
/// ### get_total_transfer_size_local
|
||||
|
||||
@@ -25,25 +25,23 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// deps
|
||||
extern crate bytesize;
|
||||
// locals
|
||||
use super::{
|
||||
actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel,
|
||||
COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE,
|
||||
COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO,
|
||||
COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS,
|
||||
COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL,
|
||||
COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE, COMPONENT_RADIO_DISCONNECT,
|
||||
COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL,
|
||||
COMPONENT_TEXT_HELP,
|
||||
COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH,
|
||||
COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX,
|
||||
COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE,
|
||||
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING,
|
||||
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
|
||||
};
|
||||
use crate::fs::explorer::FileSorting;
|
||||
use crate::fs::FsEntry;
|
||||
use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder};
|
||||
use crate::ui::keymap::*;
|
||||
use crate::utils::fmt::fmt_path_elide_ex;
|
||||
// externals
|
||||
use std::path::{Path, PathBuf};
|
||||
use tuirealm::{
|
||||
components::progress_bar::ProgressBarPropsBuilder,
|
||||
props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
|
||||
@@ -87,7 +85,7 @@ impl Update for FileTransferActivity {
|
||||
entry = Some(e.clone());
|
||||
}
|
||||
if let Some(entry) = entry {
|
||||
if self.action_enter_local_dir(entry, false) {
|
||||
if self.action_submit_local(entry) {
|
||||
// Update file list if sync
|
||||
if self.browser.sync_browsing {
|
||||
let _ = self.update_remote_filelist();
|
||||
@@ -120,8 +118,7 @@ impl Update for FileTransferActivity {
|
||||
}
|
||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_L) => {
|
||||
// Reload directory
|
||||
let pwd: PathBuf = self.local().wrkdir.clone();
|
||||
self.local_scan(pwd.as_path());
|
||||
self.reload_local_dir();
|
||||
// Reload file list component
|
||||
self.update_local_filelist()
|
||||
}
|
||||
@@ -152,7 +149,7 @@ impl Update for FileTransferActivity {
|
||||
entry = Some(e.clone());
|
||||
}
|
||||
if let Some(entry) = entry {
|
||||
if self.action_enter_remote_dir(entry, false) {
|
||||
if self.action_submit_remote(entry) {
|
||||
// Update file list if sync
|
||||
if self.browser.sync_browsing {
|
||||
let _ = self.update_local_filelist();
|
||||
@@ -195,8 +192,7 @@ impl Update for FileTransferActivity {
|
||||
}
|
||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_L) => {
|
||||
// Reload directory
|
||||
let pwd: PathBuf = self.remote().wrkdir.clone();
|
||||
self.remote_scan(pwd.as_path());
|
||||
self.reload_remote_dir();
|
||||
// Reload file list component
|
||||
self.update_remote_filelist()
|
||||
}
|
||||
@@ -270,6 +266,26 @@ impl Update for FileTransferActivity {
|
||||
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) => {
|
||||
// View
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_open_local(),
|
||||
FileExplorerTab::Remote => self.action_open_remote(),
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
self.action_find_open()
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_W)
|
||||
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_W)
|
||||
| (COMPONENT_EXPLORER_FIND, &MSG_KEY_CHAR_W) => {
|
||||
// Open with
|
||||
self.mount_openwith();
|
||||
None
|
||||
}
|
||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_X)
|
||||
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_X) => {
|
||||
// Mount exec
|
||||
@@ -350,12 +366,14 @@ impl Update for FileTransferActivity {
|
||||
}
|
||||
(COMPONENT_INPUT_COPY, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||
// Copy file
|
||||
self.umount_copy();
|
||||
self.mount_blocking_wait("Copying file(s)…");
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_copy(input.to_string()),
|
||||
FileExplorerTab::Remote => self.action_remote_copy(input.to_string()),
|
||||
_ => panic!("Found tab doesn't support COPY"),
|
||||
}
|
||||
self.umount_copy();
|
||||
self.umount_wait();
|
||||
// Reload files
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.update_local_filelist(),
|
||||
@@ -363,6 +381,7 @@ impl Update for FileTransferActivity {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(COMPONENT_INPUT_COPY, _) => None,
|
||||
// -- exec popup
|
||||
(COMPONENT_INPUT_EXEC, &MSG_KEY_ESC) => {
|
||||
self.umount_exec();
|
||||
@@ -370,12 +389,14 @@ impl Update for FileTransferActivity {
|
||||
}
|
||||
(COMPONENT_INPUT_EXEC, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||
// Exex command
|
||||
self.umount_exec();
|
||||
self.mount_blocking_wait(format!("Executing '{}'…", input).as_str());
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_exec(input.to_string()),
|
||||
FileExplorerTab::Remote => self.action_remote_exec(input.to_string()),
|
||||
_ => panic!("Found tab doesn't support EXEC"),
|
||||
}
|
||||
self.umount_exec();
|
||||
self.umount_wait();
|
||||
// Reload files
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.update_local_filelist(),
|
||||
@@ -383,6 +404,7 @@ impl Update for FileTransferActivity {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(COMPONENT_INPUT_EXEC, _) => None,
|
||||
// -- find popup
|
||||
(COMPONENT_INPUT_FIND, &MSG_KEY_ESC) => {
|
||||
self.umount_find_input();
|
||||
@@ -450,6 +472,7 @@ impl Update for FileTransferActivity {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(COMPONENT_INPUT_GOTO, _) => None,
|
||||
// -- make directory
|
||||
(COMPONENT_INPUT_MKDIR, &MSG_KEY_ESC) => {
|
||||
self.umount_mkdir();
|
||||
@@ -469,6 +492,7 @@ impl Update for FileTransferActivity {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(COMPONENT_INPUT_MKDIR, _) => None,
|
||||
// -- new file
|
||||
(COMPONENT_INPUT_NEWFILE, &MSG_KEY_ESC) => {
|
||||
self.umount_newfile();
|
||||
@@ -488,18 +512,38 @@ impl Update for FileTransferActivity {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(COMPONENT_INPUT_NEWFILE, _) => None,
|
||||
// -- open with
|
||||
(COMPONENT_INPUT_OPEN_WITH, &MSG_KEY_ESC) => {
|
||||
self.umount_openwith();
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_OPEN_WITH, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_open_with(input),
|
||||
FileExplorerTab::Remote => self.action_remote_open_with(input),
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
self.action_find_open_with(input)
|
||||
}
|
||||
}
|
||||
self.umount_openwith();
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_OPEN_WITH, _) => None,
|
||||
// -- rename
|
||||
(COMPONENT_INPUT_RENAME, &MSG_KEY_ESC) => {
|
||||
self.umount_rename();
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_RENAME, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||
self.umount_rename();
|
||||
self.mount_blocking_wait("Moving file(s)…");
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_rename(input.to_string()),
|
||||
FileExplorerTab::Remote => self.action_remote_rename(input.to_string()),
|
||||
_ => panic!("Found tab doesn't support RENAME"),
|
||||
}
|
||||
self.umount_rename();
|
||||
self.umount_wait();
|
||||
// Reload files
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.update_local_filelist(),
|
||||
@@ -507,6 +551,7 @@ impl Update for FileTransferActivity {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(COMPONENT_INPUT_RENAME, _) => None,
|
||||
// -- save as
|
||||
(COMPONENT_INPUT_SAVEAS, &MSG_KEY_ESC) => {
|
||||
self.umount_saveas();
|
||||
@@ -531,12 +576,14 @@ impl Update for FileTransferActivity {
|
||||
FileExplorerTab::FindRemote => self.update_local_filelist(),
|
||||
}
|
||||
}
|
||||
(COMPONENT_INPUT_SAVEAS, _) => None,
|
||||
// -- fileinfo
|
||||
(COMPONENT_LIST_FILEINFO, &MSG_KEY_ENTER)
|
||||
| (COMPONENT_LIST_FILEINFO, &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)))) => {
|
||||
@@ -545,6 +592,8 @@ impl Update for FileTransferActivity {
|
||||
}
|
||||
(COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||
// Choice is 'YES'
|
||||
self.umount_radio_delete();
|
||||
self.mount_blocking_wait("Removing file(s)…");
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_delete(),
|
||||
FileExplorerTab::Remote => self.action_remote_delete(),
|
||||
@@ -571,7 +620,7 @@ impl Update for FileTransferActivity {
|
||||
self.update_find_list();
|
||||
}
|
||||
}
|
||||
self.umount_radio_delete();
|
||||
self.umount_wait();
|
||||
// Reload files
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.update_local_filelist(),
|
||||
@@ -580,6 +629,7 @@ impl Update for FileTransferActivity {
|
||||
FileExplorerTab::FindRemote => self.update_remote_filelist(),
|
||||
}
|
||||
}
|
||||
(COMPONENT_RADIO_DELETE, _) => None,
|
||||
// -- disconnect
|
||||
(COMPONENT_RADIO_DISCONNECT, &MSG_KEY_ESC)
|
||||
| (COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
|
||||
@@ -591,6 +641,7 @@ impl Update for FileTransferActivity {
|
||||
self.umount_disconnect();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_DISCONNECT, _) => None,
|
||||
// -- quit
|
||||
(COMPONENT_RADIO_QUIT, &MSG_KEY_ESC)
|
||||
| (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
|
||||
@@ -602,6 +653,7 @@ impl Update for FileTransferActivity {
|
||||
self.umount_quit();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_QUIT, _) => None,
|
||||
// -- sorting
|
||||
(COMPONENT_RADIO_SORTING, &MSG_KEY_ESC)
|
||||
| (COMPONENT_RADIO_SORTING, Msg::OnSubmit(_)) => {
|
||||
@@ -634,27 +686,32 @@ impl Update for FileTransferActivity {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(COMPONENT_RADIO_SORTING, _) => None,
|
||||
// -- error
|
||||
(COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) => {
|
||||
self.umount_error();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_ERROR, _) => None,
|
||||
// -- fatal
|
||||
(COMPONENT_TEXT_FATAL, &MSG_KEY_ESC) | (COMPONENT_TEXT_FATAL, &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) => {
|
||||
self.umount_help();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_HELP, _) => None,
|
||||
// -- progress bar
|
||||
(COMPONENT_PROGRESS_BAR_PARTIAL, &MSG_KEY_CTRL_C) => {
|
||||
// Set transfer aborted to True
|
||||
self.transfer.abort();
|
||||
None
|
||||
}
|
||||
(COMPONENT_PROGRESS_BAR_PARTIAL, _) => None,
|
||||
// -- fallback
|
||||
(_, _) => None, // Nothing to do
|
||||
},
|
||||
@@ -671,10 +728,8 @@ impl FileTransferActivity {
|
||||
Some(props) => {
|
||||
// Get width
|
||||
let width: usize = self
|
||||
.context
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.store
|
||||
.context()
|
||||
.store()
|
||||
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
|
||||
.unwrap_or(256);
|
||||
let hostname: String = match hostname::get() {
|
||||
@@ -688,12 +743,7 @@ impl FileTransferActivity {
|
||||
let hostname: String = format!(
|
||||
"{}:{} ",
|
||||
hostname,
|
||||
FileTransferActivity::elide_wrkdir_path(
|
||||
self.local().wrkdir.as_path(),
|
||||
hostname.as_str(),
|
||||
width
|
||||
)
|
||||
.display()
|
||||
fmt_path_elide_ex(self.local().wrkdir.as_path(), width, hostname.len() + 3) // 3 because of '/…/'
|
||||
);
|
||||
let files: Vec<String> = self
|
||||
.local()
|
||||
@@ -719,22 +769,19 @@ impl FileTransferActivity {
|
||||
Some(props) => {
|
||||
// Get width
|
||||
let width: usize = self
|
||||
.context
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.store
|
||||
.context()
|
||||
.store()
|
||||
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
|
||||
.unwrap_or(256);
|
||||
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
|
||||
let params = self.context().ft_params().unwrap();
|
||||
let hostname: String = format!(
|
||||
"{}:{} ",
|
||||
params.address,
|
||||
FileTransferActivity::elide_wrkdir_path(
|
||||
fmt_path_elide_ex(
|
||||
self.remote().wrkdir.as_path(),
|
||||
params.address.as_str(),
|
||||
width
|
||||
width,
|
||||
params.address.len() + 3 // 3 because of '/…/'
|
||||
)
|
||||
.display()
|
||||
);
|
||||
let files: Vec<String> = self
|
||||
.remote()
|
||||
@@ -857,38 +904,4 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### elide_wrkdir_path
|
||||
///
|
||||
/// Elide working directory path if longer than width + host.len
|
||||
/// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME}
|
||||
fn elide_wrkdir_path(wrkdir: &Path, host: &str, width: usize) -> PathBuf {
|
||||
let fmt_path: String = format!("{}", wrkdir.display());
|
||||
// NOTE: +5 is const
|
||||
match fmt_path.len() + host.len() + 5 > width {
|
||||
false => PathBuf::from(wrkdir),
|
||||
true => {
|
||||
// Elide
|
||||
let ancestors_len: usize = wrkdir.ancestors().count();
|
||||
let mut ancestors = wrkdir.ancestors();
|
||||
let mut elided_path: PathBuf = PathBuf::new();
|
||||
// If ancestors_len's size is bigger than 2, push count - 2
|
||||
if ancestors_len > 2 {
|
||||
elided_path.push(ancestors.nth(ancestors_len - 2).unwrap());
|
||||
}
|
||||
// If ancestors_len is bigger than 3, push '...' and parent too
|
||||
if ancestors_len > 3 {
|
||||
elided_path.push("...");
|
||||
if let Some(parent) = wrkdir.ancestors().nth(1) {
|
||||
elided_path.push(parent.file_name().unwrap());
|
||||
}
|
||||
}
|
||||
// Push file_name
|
||||
if let Some(name) = wrkdir.file_name() {
|
||||
elided_path.push(name);
|
||||
}
|
||||
elided_path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
extern crate bytesize;
|
||||
extern crate hostname;
|
||||
#[cfg(target_family = "unix")]
|
||||
extern crate users;
|
||||
// locals
|
||||
use super::{browser::FileExplorerTab, Context, FileTransferActivity};
|
||||
use crate::fs::explorer::FileSorting;
|
||||
@@ -70,13 +65,22 @@ impl FileTransferActivity {
|
||||
/// Initialize file transfer activity's view
|
||||
pub(super) fn init(&mut self) {
|
||||
// Mount local file explorer
|
||||
let local_explorer_background = self.theme().transfer_local_explorer_background;
|
||||
let local_explorer_foreground = self.theme().transfer_local_explorer_foreground;
|
||||
let local_explorer_highlighted = self.theme().transfer_local_explorer_highlighted;
|
||||
let remote_explorer_background = self.theme().transfer_remote_explorer_background;
|
||||
let remote_explorer_foreground = self.theme().transfer_remote_explorer_foreground;
|
||||
let remote_explorer_highlighted = self.theme().transfer_remote_explorer_highlighted;
|
||||
let log_panel = self.theme().transfer_log_window;
|
||||
let log_background = self.theme().transfer_log_background;
|
||||
self.view.mount(
|
||||
super::COMPONENT_EXPLORER_LOCAL,
|
||||
Box::new(FileList::new(
|
||||
FileListPropsBuilder::default()
|
||||
.with_background(Color::Yellow)
|
||||
.with_foreground(Color::Yellow)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::Yellow)
|
||||
.with_highlight_color(local_explorer_highlighted)
|
||||
.with_background(local_explorer_background)
|
||||
.with_foreground(local_explorer_foreground)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, local_explorer_highlighted)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -85,9 +89,10 @@ impl FileTransferActivity {
|
||||
super::COMPONENT_EXPLORER_REMOTE,
|
||||
Box::new(FileList::new(
|
||||
FileListPropsBuilder::default()
|
||||
.with_background(Color::LightBlue)
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightBlue)
|
||||
.with_highlight_color(remote_explorer_highlighted)
|
||||
.with_background(remote_explorer_background)
|
||||
.with_foreground(remote_explorer_foreground)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, remote_explorer_highlighted)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -96,7 +101,8 @@ impl FileTransferActivity {
|
||||
super::COMPONENT_LOG_BOX,
|
||||
Box::new(LogBox::new(
|
||||
LogboxPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||
.with_background(log_background)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, log_panel)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -236,6 +242,14 @@ impl FileTransferActivity {
|
||||
self.view.render(super::COMPONENT_INPUT_NEWFILE, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_OPEN_WITH) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_INPUT_OPEN_WITH, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
@@ -366,12 +380,13 @@ impl FileTransferActivity {
|
||||
/// Mount error box
|
||||
pub(super) fn mount_error(&mut self, text: &str) {
|
||||
// Mount
|
||||
let error_color = self.theme().misc_error_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_ERROR,
|
||||
Box::new(MsgBox::new(
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
|
||||
.with_foreground(error_color)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, error_color)
|
||||
.bold()
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
@@ -390,12 +405,13 @@ impl FileTransferActivity {
|
||||
|
||||
pub(super) fn mount_fatal(&mut self, text: &str) {
|
||||
// Mount
|
||||
let error_color = self.theme().misc_error_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_FATAL,
|
||||
Box::new(MsgBox::new(
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
|
||||
.with_foreground(error_color)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, error_color)
|
||||
.bold()
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
@@ -406,17 +422,28 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_wait(&mut self, text: &str) {
|
||||
self.mount_wait_ex(text, false, Color::Reset);
|
||||
}
|
||||
|
||||
pub(super) fn mount_blocking_wait(&mut self, text: &str) {
|
||||
self.mount_wait_ex(text, true, Color::Reset);
|
||||
self.view();
|
||||
}
|
||||
|
||||
fn mount_wait_ex(&mut self, text: &str, blink: bool, color: Color) {
|
||||
// Mount
|
||||
let mut builder: MsgBoxPropsBuilder = MsgBoxPropsBuilder::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();
|
||||
}
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_WAIT,
|
||||
Box::new(MsgBox::new(
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::White)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.bold()
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
)),
|
||||
Box::new(MsgBox::new(builder.build())),
|
||||
);
|
||||
// Give focus to info
|
||||
self.view.active(super::COMPONENT_TEXT_WAIT);
|
||||
@@ -431,13 +458,14 @@ impl FileTransferActivity {
|
||||
/// Mount quit popup
|
||||
pub(super) fn mount_quit(&mut self) {
|
||||
// Protocol
|
||||
let quit_color = self.theme().misc_quit_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_QUIT,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Yellow)
|
||||
.with_color(quit_color)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||
.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")],
|
||||
@@ -460,13 +488,14 @@ impl FileTransferActivity {
|
||||
/// Mount disconnect popup
|
||||
pub(super) fn mount_disconnect(&mut self) {
|
||||
// Protocol
|
||||
let quit_color = self.theme().misc_quit_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_DISCONNECT,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Yellow)
|
||||
.with_color(quit_color)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||
.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")],
|
||||
@@ -485,12 +514,14 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_copy(&mut self) {
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_COPY,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("Copy file(s) to..."))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
|
||||
.with_foreground(input_color)
|
||||
.with_label(String::from("Copy file(s) to…"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -502,11 +533,13 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_exec(&mut self) {
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_EXEC,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::White)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
|
||||
.with_foreground(input_color)
|
||||
.with_label(String::from("Execute command"))
|
||||
.build(),
|
||||
)),
|
||||
@@ -520,9 +553,17 @@ impl FileTransferActivity {
|
||||
|
||||
pub(super) fn mount_find(&mut self, search: &str) {
|
||||
// Get color
|
||||
let color: Color = match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => Color::Yellow,
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => Color::LightBlue,
|
||||
let (bg, fg, hg): (Color, Color, Color) = match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => (
|
||||
self.theme().transfer_local_explorer_background,
|
||||
self.theme().transfer_local_explorer_foreground,
|
||||
self.theme().transfer_local_explorer_highlighted,
|
||||
),
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => (
|
||||
self.theme().transfer_remote_explorer_background,
|
||||
self.theme().transfer_remote_explorer_foreground,
|
||||
self.theme().transfer_remote_explorer_highlighted,
|
||||
),
|
||||
};
|
||||
// Mount component
|
||||
self.view.mount(
|
||||
@@ -530,9 +571,10 @@ impl FileTransferActivity {
|
||||
Box::new(FileList::new(
|
||||
FileListPropsBuilder::default()
|
||||
.with_files(Some(format!("Search results for \"{}\"", search)), vec![])
|
||||
.with_borders(Borders::ALL, BorderType::Plain, color)
|
||||
.with_background(color)
|
||||
.with_foreground(color)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, hg)
|
||||
.with_highlight_color(hg)
|
||||
.with_background(bg)
|
||||
.with_foreground(fg)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -545,11 +587,13 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_find_input(&mut self) {
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_FIND,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
|
||||
.with_foreground(input_color)
|
||||
.with_label(String::from("Search files by name"))
|
||||
.build(),
|
||||
)),
|
||||
@@ -564,11 +608,13 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_goto(&mut self) {
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_GOTO,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
|
||||
.with_foreground(input_color)
|
||||
.with_label(String::from("Change working directory"))
|
||||
.build(),
|
||||
)),
|
||||
@@ -581,11 +627,13 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_mkdir(&mut self) {
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_MKDIR,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
|
||||
.with_foreground(input_color)
|
||||
.with_label(String::from("Insert directory name"))
|
||||
.build(),
|
||||
)),
|
||||
@@ -598,11 +646,13 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_newfile(&mut self) {
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_NEWFILE,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
|
||||
.with_foreground(input_color)
|
||||
.with_label(String::from("New file name"))
|
||||
.build(),
|
||||
)),
|
||||
@@ -614,13 +664,34 @@ impl FileTransferActivity {
|
||||
self.view.umount(super::COMPONENT_INPUT_NEWFILE);
|
||||
}
|
||||
|
||||
pub(super) fn mount_openwith(&mut self) {
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_OPEN_WITH,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
|
||||
.with_foreground(input_color)
|
||||
.with_label(String::from("Open file with…"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.active(super::COMPONENT_INPUT_OPEN_WITH);
|
||||
}
|
||||
|
||||
pub(super) fn umount_openwith(&mut self) {
|
||||
self.view.umount(super::COMPONENT_INPUT_OPEN_WITH);
|
||||
}
|
||||
|
||||
pub(super) fn mount_rename(&mut self) {
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_RENAME,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("Move file(s) to..."))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
|
||||
.with_foreground(input_color)
|
||||
.with_label(String::from("Move file(s) to…"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -632,12 +703,14 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_saveas(&mut self) {
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_SAVEAS,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||
.with_label(String::from("Save as..."))
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
|
||||
.with_foreground(input_color)
|
||||
.with_label(String::from("Save as…"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
@@ -649,11 +722,13 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_progress_bar(&mut self, root_name: String) {
|
||||
let prog_color_full = self.theme().transfer_progress_bar_full;
|
||||
let prog_color_partial = self.theme().transfer_progress_bar_partial;
|
||||
self.view.mount(
|
||||
super::COMPONENT_PROGRESS_BAR_FULL,
|
||||
Box::new(ProgressBar::new(
|
||||
ProgressBarPropsBuilder::default()
|
||||
.with_progbar_color(Color::Green)
|
||||
.with_progbar_color(prog_color_full)
|
||||
.with_background(Color::Black)
|
||||
.with_borders(
|
||||
Borders::TOP | Borders::RIGHT | Borders::LEFT,
|
||||
@@ -668,7 +743,7 @@ impl FileTransferActivity {
|
||||
super::COMPONENT_PROGRESS_BAR_PARTIAL,
|
||||
Box::new(ProgressBar::new(
|
||||
ProgressBarPropsBuilder::default()
|
||||
.with_progbar_color(Color::Green)
|
||||
.with_progbar_color(prog_color_partial)
|
||||
.with_background(Color::Black)
|
||||
.with_borders(
|
||||
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
|
||||
@@ -688,6 +763,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_file_sorting(&mut self) {
|
||||
let sorting_color = self.theme().transfer_status_sorting;
|
||||
let sorting: FileSorting = match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.local().get_file_sorting(),
|
||||
FileExplorerTab::Remote => self.remote().get_file_sorting(),
|
||||
@@ -703,9 +779,9 @@ impl FileTransferActivity {
|
||||
super::COMPONENT_RADIO_SORTING,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightMagenta)
|
||||
.with_color(sorting_color)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, sorting_color)
|
||||
.with_options(
|
||||
Some(String::from("Sort files by")),
|
||||
vec![
|
||||
@@ -727,13 +803,14 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn mount_radio_delete(&mut self) {
|
||||
let warn_color = self.theme().misc_warn_dialog;
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_DELETE,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::Red)
|
||||
.with_color(warn_color)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::Red)
|
||||
.with_borders(Borders::ALL, BorderType::Plain, warn_color)
|
||||
.with_options(
|
||||
Some(String::from("Delete file")),
|
||||
vec![String::from("Yes"), String::from("No")],
|
||||
@@ -861,21 +938,23 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn refresh_local_status_bar(&mut self) {
|
||||
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(Color::LightYellow)
|
||||
.with_foreground(sorting_color)
|
||||
.build(),
|
||||
TextSpanBuilder::new(Self::get_file_sorting_str(self.local().get_file_sorting()))
|
||||
.with_foreground(Color::LightYellow)
|
||||
.with_foreground(sorting_color)
|
||||
.reversed()
|
||||
.build(),
|
||||
TextSpanBuilder::new(" Hidden files: ")
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_foreground(hidden_color)
|
||||
.build(),
|
||||
TextSpanBuilder::new(Self::get_hidden_files_str(
|
||||
self.local().hidden_files_visible(),
|
||||
))
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_foreground(hidden_color)
|
||||
.reversed()
|
||||
.build(),
|
||||
];
|
||||
@@ -890,31 +969,34 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(super) fn refresh_remote_status_bar(&mut self) {
|
||||
let sorting_color = self.theme().transfer_status_sorting;
|
||||
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(Color::LightYellow)
|
||||
.with_foreground(sorting_color)
|
||||
.build(),
|
||||
TextSpanBuilder::new(Self::get_file_sorting_str(self.remote().get_file_sorting()))
|
||||
.with_foreground(Color::LightYellow)
|
||||
.with_foreground(sorting_color)
|
||||
.reversed()
|
||||
.build(),
|
||||
TextSpanBuilder::new(" Hidden files: ")
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_foreground(hidden_color)
|
||||
.build(),
|
||||
TextSpanBuilder::new(Self::get_hidden_files_str(
|
||||
self.remote().hidden_files_visible(),
|
||||
))
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_foreground(hidden_color)
|
||||
.reversed()
|
||||
.build(),
|
||||
TextSpanBuilder::new(" Sync Browsing: ")
|
||||
.with_foreground(Color::LightGreen)
|
||||
.with_foreground(sync_color)
|
||||
.build(),
|
||||
TextSpanBuilder::new(match self.browser.sync_browsing {
|
||||
true => "ON ",
|
||||
false => "OFF",
|
||||
})
|
||||
.with_foreground(Color::LightGreen)
|
||||
.with_foreground(sync_color)
|
||||
.reversed()
|
||||
.build(),
|
||||
];
|
||||
@@ -932,6 +1014,7 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Mount help
|
||||
pub(super) fn mount_help(&mut self) {
|
||||
let key_color = self.theme().misc_keys;
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_HELP,
|
||||
Box::new(Scrolltable::new(
|
||||
@@ -946,7 +1029,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<ESC>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Disconnect"))
|
||||
@@ -954,7 +1037,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<TAB>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(
|
||||
@@ -964,7 +1047,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<BACKSPACE>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Go to previous directory"))
|
||||
@@ -972,7 +1055,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<RIGHT/LEFT>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Change explorer tab"))
|
||||
@@ -980,7 +1063,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<UP/DOWN>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Move up/down in list"))
|
||||
@@ -988,7 +1071,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<ENTER>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Enter directory"))
|
||||
@@ -996,7 +1079,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<SPACE>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Upload/Download file"))
|
||||
@@ -1004,7 +1087,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<A>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Toggle hidden files"))
|
||||
@@ -1012,7 +1095,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<B>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Change file sorting mode"))
|
||||
@@ -1020,7 +1103,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<C>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Copy"))
|
||||
@@ -1028,7 +1111,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<D>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Make directory"))
|
||||
@@ -1036,7 +1119,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<G>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Go to path"))
|
||||
@@ -1044,7 +1127,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<H>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Show help"))
|
||||
@@ -1052,7 +1135,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<I>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Show info about selected file"))
|
||||
@@ -1060,7 +1143,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<L>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Reload directory content"))
|
||||
@@ -1068,7 +1151,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<M>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Select file"))
|
||||
@@ -1076,7 +1159,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<N>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Create new file"))
|
||||
@@ -1084,15 +1167,17 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<O>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Open text file"))
|
||||
.add_col(TextSpan::from(
|
||||
" Open text file with preferred editor",
|
||||
))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<Q>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Quit termscp"))
|
||||
@@ -1100,7 +1185,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<R>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Rename file"))
|
||||
@@ -1108,7 +1193,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<S>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Save file as"))
|
||||
@@ -1116,15 +1201,35 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<U>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Go to parent directory"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<V>")
|
||||
.bold()
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.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::from(
|
||||
" Open file with specified application",
|
||||
))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<X>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Execute shell command"))
|
||||
@@ -1132,7 +1237,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<Y>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Toggle synchronized browsing"))
|
||||
@@ -1140,7 +1245,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<DEL|E>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Delete selected file"))
|
||||
@@ -1148,7 +1253,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<CTRL+A>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Select all files"))
|
||||
@@ -1156,7 +1261,7 @@ impl FileTransferActivity {
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<CTRL+C>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.with_foreground(key_color)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Interrupt file transfer"))
|
||||
|
||||
@@ -27,22 +27,80 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::SetupActivity;
|
||||
use super::{SetupActivity, ViewLayout};
|
||||
// Ext
|
||||
use crate::config::themes::Theme;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::env;
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::{Payload, Value};
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### action_on_esc
|
||||
///
|
||||
/// On <ESC>, if there are changes in the configuration, the quit dialog must be shown, otherwise
|
||||
/// we can exit without any problem
|
||||
pub(super) fn action_on_esc(&mut self) {
|
||||
if self.config_changed() {
|
||||
self.mount_quit();
|
||||
} else {
|
||||
self.exit_reason = Some(super::ExitReason::Quit);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### action_save_all
|
||||
///
|
||||
/// Save all configurations. If current tab can load values, they will be loaded, otherwise they'll just be saved.
|
||||
/// Once all the configuration has been changed, set config_changed to false
|
||||
pub(super) fn action_save_all(&mut self) -> Result<(), String> {
|
||||
self.action_save_config()?;
|
||||
self.action_save_theme()?;
|
||||
// Set config changed to false
|
||||
self.set_config_changed(false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### action_save_config
|
||||
///
|
||||
/// Save configuration
|
||||
pub(super) fn action_save_config(&mut self) -> Result<(), String> {
|
||||
// Collect input values
|
||||
self.collect_input_values();
|
||||
fn action_save_config(&mut self) -> Result<(), String> {
|
||||
// Collect input values if in setup form
|
||||
if self.layout == ViewLayout::SetupForm {
|
||||
self.collect_input_values();
|
||||
}
|
||||
self.save_config()
|
||||
}
|
||||
|
||||
/// ### action_save_theme
|
||||
///
|
||||
/// Save configuration
|
||||
fn action_save_theme(&mut self) -> Result<(), String> {
|
||||
// Collect input values if in theme form
|
||||
if self.layout == ViewLayout::Theme {
|
||||
self.collect_styles()
|
||||
.map_err(|e| format!("'{}' has an invalid color", e))?;
|
||||
}
|
||||
// save theme
|
||||
self.save_theme()
|
||||
}
|
||||
|
||||
/// ### action_change_tab
|
||||
///
|
||||
/// Change view tab and load input values in order not to lose them
|
||||
pub(super) fn action_change_tab(&mut self, new_tab: ViewLayout) -> Result<(), String> {
|
||||
// load values for current tab first
|
||||
match self.layout {
|
||||
ViewLayout::SetupForm => self.collect_input_values(),
|
||||
ViewLayout::Theme => self
|
||||
.collect_styles()
|
||||
.map_err(|e| format!("'{}' has an invalid color", e))?,
|
||||
_ => {}
|
||||
}
|
||||
// Update view
|
||||
self.init(new_tab);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### action_reset_config
|
||||
///
|
||||
/// Reset configuration input fields
|
||||
@@ -56,37 +114,47 @@ impl SetupActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### action_reset_theme
|
||||
///
|
||||
/// Reset configuration input fields
|
||||
pub(super) fn action_reset_theme(&mut self) -> Result<(), String> {
|
||||
match self.reset_theme_changes() {
|
||||
Err(err) => Err(err),
|
||||
Ok(_) => {
|
||||
self.load_styles();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### action_delete_ssh_key
|
||||
///
|
||||
/// delete of a ssh key
|
||||
pub(super) fn action_delete_ssh_key(&mut self) {
|
||||
// Get key
|
||||
if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
// get index
|
||||
let idx: Option<usize> = match self.view.get_state(super::COMPONENT_LIST_SSH_KEYS) {
|
||||
Some(Payload::One(Value::Usize(idx))) => Some(idx),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(idx) = idx {
|
||||
let key: Option<String> = config_cli.iter_ssh_keys().nth(idx).cloned();
|
||||
if let Some(key) = key {
|
||||
match config_cli.get_ssh_key(&key) {
|
||||
Ok(opt) => {
|
||||
if let Some((host, username, _)) = opt {
|
||||
if let Err(err) =
|
||||
self.delete_ssh_key(host.as_str(), username.as_str())
|
||||
{
|
||||
// Report error
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
// get index
|
||||
let idx: Option<usize> = match self.view.get_state(super::COMPONENT_LIST_SSH_KEYS) {
|
||||
Some(Payload::One(Value::Usize(idx))) => Some(idx),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(idx) = idx {
|
||||
let key: Option<String> = self.config().iter_ssh_keys().nth(idx).cloned();
|
||||
if let Some(key) = key {
|
||||
match self.config().get_ssh_key(&key) {
|
||||
Ok(opt) => {
|
||||
if let Some((host, username, _)) = opt {
|
||||
if let Err(err) = self.delete_ssh_key(host.as_str(), username.as_str())
|
||||
{
|
||||
// Report error
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
self.mount_error(
|
||||
format!("Could not get ssh key \"{}\": {}", key, err).as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
self.mount_error(
|
||||
format!("Could not get ssh key \"{}\": {}", key, err).as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,66 +165,150 @@ impl SetupActivity {
|
||||
///
|
||||
/// Create a new ssh key
|
||||
pub(super) fn action_new_ssh_key(&mut self) {
|
||||
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
// get parameters
|
||||
let host: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_HOST) {
|
||||
Some(Payload::One(Value::Str(host))) => host,
|
||||
_ => String::new(),
|
||||
};
|
||||
let username: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_USERNAME) {
|
||||
Some(Payload::One(Value::Str(user))) => user,
|
||||
_ => String::new(),
|
||||
};
|
||||
// Prepare text editor
|
||||
env::set_var("EDITOR", cli.get_text_editor());
|
||||
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
||||
// Put input mode back to normal
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
// Leave alternate mode
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
ctx.leave_alternate_screen();
|
||||
}
|
||||
// Re-enable raw mode
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Write key to file
|
||||
match edit::edit(placeholder.as_bytes()) {
|
||||
Ok(rsa_key) => {
|
||||
// Remove placeholder from `rsa_key`
|
||||
let rsa_key: String = rsa_key.as_str().replace(placeholder.as_str(), "");
|
||||
if rsa_key.is_empty() {
|
||||
// Report error: empty key
|
||||
self.mount_error("SSH key is empty!");
|
||||
} else {
|
||||
// Add key
|
||||
if let Err(err) =
|
||||
self.add_ssh_key(host.as_str(), username.as_str(), rsa_key.as_str())
|
||||
{
|
||||
self.mount_error(
|
||||
format!("Could not create new private key: {}", err).as_str(),
|
||||
);
|
||||
}
|
||||
// get parameters
|
||||
let host: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_HOST) {
|
||||
Some(Payload::One(Value::Str(host))) => host,
|
||||
_ => String::new(),
|
||||
};
|
||||
let username: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_USERNAME) {
|
||||
Some(Payload::One(Value::Str(user))) => user,
|
||||
_ => String::new(),
|
||||
};
|
||||
// Prepare text editor
|
||||
env::set_var("EDITOR", self.config().get_text_editor());
|
||||
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
||||
// Put input mode back to normal
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
// Leave alternate mode
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
ctx.leave_alternate_screen();
|
||||
}
|
||||
// Re-enable raw mode
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Write key to file
|
||||
match edit::edit(placeholder.as_bytes()) {
|
||||
Ok(rsa_key) => {
|
||||
// Remove placeholder from `rsa_key`
|
||||
let rsa_key: String = rsa_key.as_str().replace(placeholder.as_str(), "");
|
||||
if rsa_key.is_empty() {
|
||||
// Report error: empty key
|
||||
self.mount_error("SSH key is empty!");
|
||||
} else {
|
||||
// Add key
|
||||
if let Err(err) =
|
||||
self.add_ssh_key(host.as_str(), username.as_str(), rsa_key.as_str())
|
||||
{
|
||||
self.mount_error(
|
||||
format!("Could not create new private key: {}", err).as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
self.mount_error(
|
||||
format!("Could not write private key to file: {}", err).as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Restore terminal
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
// Enter alternate mode
|
||||
ctx.enter_alternate_screen();
|
||||
Err(err) => {
|
||||
// Report error
|
||||
self.mount_error(format!("Could not write private key to file: {}", err).as_str());
|
||||
}
|
||||
}
|
||||
// Restore terminal
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
// Enter alternate mode
|
||||
ctx.enter_alternate_screen();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_color
|
||||
///
|
||||
/// Given a component and a color, save the color into the theme
|
||||
pub(super) fn action_save_color(&mut self, component: &str, color: Color) {
|
||||
let theme: &mut Theme = self.theme_mut();
|
||||
match component {
|
||||
super::COMPONENT_COLOR_AUTH_ADDR => {
|
||||
theme.auth_address = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_BOOKMARKS => {
|
||||
theme.auth_bookmarks = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_PASSWORD => {
|
||||
theme.auth_password = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_PORT => {
|
||||
theme.auth_port = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_PROTOCOL => {
|
||||
theme.auth_protocol = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_RECENTS => {
|
||||
theme.auth_recents = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_AUTH_USERNAME => {
|
||||
theme.auth_username = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_ERROR => {
|
||||
theme.misc_error_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_INPUT => {
|
||||
theme.misc_input_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_KEYS => {
|
||||
theme.misc_keys = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_QUIT => {
|
||||
theme.misc_quit_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_SAVE => {
|
||||
theme.misc_save_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_MISC_WARN => {
|
||||
theme.misc_warn_dialog = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG => {
|
||||
theme.transfer_local_explorer_background = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG => {
|
||||
theme.transfer_local_explorer_foreground = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG => {
|
||||
theme.transfer_local_explorer_highlighted = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG => {
|
||||
theme.transfer_remote_explorer_background = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG => {
|
||||
theme.transfer_remote_explorer_foreground = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG => {
|
||||
theme.transfer_remote_explorer_highlighted = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_BG => {
|
||||
theme.transfer_log_background = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_WIN => {
|
||||
theme.transfer_log_window = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL => {
|
||||
theme.transfer_progress_bar_full = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL => {
|
||||
theme.transfer_progress_bar_partial = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN => {
|
||||
theme.transfer_status_hidden = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING => {
|
||||
theme.transfer_status_sorting = color;
|
||||
}
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC => {
|
||||
theme.transfer_status_sync_browsing = color;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,12 +37,9 @@ impl SetupActivity {
|
||||
///
|
||||
/// Save configuration
|
||||
pub(super) fn save_config(&mut self) -> Result<(), String> {
|
||||
match self.context.as_ref().unwrap().config_client.as_ref() {
|
||||
Some(cli) => match cli.write_config() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Could not save configuration: {}", err)),
|
||||
},
|
||||
None => Ok(()),
|
||||
match self.config().write_config() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Could not save configuration: {}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,28 +48,39 @@ impl SetupActivity {
|
||||
/// Reset configuration changes; pratically read config from file, overwriting any change made
|
||||
/// since last write action
|
||||
pub(super) fn reset_config_changes(&mut self) -> Result<(), String> {
|
||||
match self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
Some(cli) => match cli.read_config() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Could not restore configuration: {}", err)),
|
||||
},
|
||||
None => Ok(()),
|
||||
}
|
||||
self.config_mut()
|
||||
.read_config()
|
||||
.map_err(|e| format!("Could not reload configuration: {}", e))
|
||||
}
|
||||
|
||||
/// ### save_theme
|
||||
///
|
||||
/// Save theme to file
|
||||
pub(super) fn save_theme(&mut self) -> Result<(), String> {
|
||||
self.theme_provider()
|
||||
.save()
|
||||
.map_err(|e| format!("Could not save theme: {}", e))
|
||||
}
|
||||
|
||||
/// ### reset_theme_changes
|
||||
///
|
||||
/// Reset changes committed to theme
|
||||
pub(super) fn reset_theme_changes(&mut self) -> Result<(), String> {
|
||||
self.theme_provider()
|
||||
.load()
|
||||
.map_err(|e| format!("Could not restore theme: {}", e))
|
||||
}
|
||||
|
||||
/// ### delete_ssh_key
|
||||
///
|
||||
/// Delete ssh key from config cli
|
||||
pub(super) fn delete_ssh_key(&mut self, host: &str, username: &str) -> Result<(), String> {
|
||||
match self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
Some(cli) => match cli.del_ssh_key(host, username) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!(
|
||||
"Could not delete ssh key \"{}@{}\": {}",
|
||||
host, username, err
|
||||
)),
|
||||
},
|
||||
None => Ok(()),
|
||||
match self.config_mut().del_ssh_key(host, username) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!(
|
||||
"Could not delete ssh key \"{}@{}\": {}",
|
||||
host, username, err
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +92,7 @@ impl SetupActivity {
|
||||
None => Ok(()),
|
||||
Some(ctx) => {
|
||||
// Set editor if config client exists
|
||||
if let Some(config_cli) = ctx.config_client.as_ref() {
|
||||
env::set_var("EDITOR", config_cli.get_text_editor());
|
||||
}
|
||||
env::set_var("EDITOR", ctx.config().get_text_editor());
|
||||
// Prepare terminal
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
@@ -95,27 +101,22 @@ impl SetupActivity {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
ctx.leave_alternate_screen();
|
||||
// Get result
|
||||
let result: Result<(), String> = match ctx.config_client.as_ref() {
|
||||
Some(config_cli) => match config_cli.iter_ssh_keys().nth(idx) {
|
||||
Some(key) => {
|
||||
// Get key path
|
||||
match config_cli.get_ssh_key(key) {
|
||||
Ok(ssh_key) => match ssh_key {
|
||||
None => Ok(()),
|
||||
Some((_, _, key_path)) => {
|
||||
match edit::edit_file(key_path.as_path()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
Err(format!("Could not edit ssh key: {}", err))
|
||||
}
|
||||
}
|
||||
let result: Result<(), String> = match ctx.config().iter_ssh_keys().nth(idx) {
|
||||
Some(key) => {
|
||||
// Get key path
|
||||
match ctx.config().get_ssh_key(key) {
|
||||
Ok(ssh_key) => match ssh_key {
|
||||
None => Ok(()),
|
||||
Some((_, _, key_path)) => {
|
||||
match edit::edit_file(key_path.as_path()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Could not edit ssh key: {}", err)),
|
||||
}
|
||||
},
|
||||
Err(err) => Err(format!("Could not read ssh key: {}", err)),
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => Err(format!("Could not read ssh key: {}", err)),
|
||||
}
|
||||
None => Ok(()),
|
||||
},
|
||||
}
|
||||
None => Ok(()),
|
||||
};
|
||||
// Restore terminal
|
||||
@@ -143,15 +144,8 @@ impl SetupActivity {
|
||||
username: &str,
|
||||
rsa_key: &str,
|
||||
) -> Result<(), String> {
|
||||
match self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
Some(cli) => {
|
||||
// Add key to client
|
||||
match cli.add_ssh_key(host, username, rsa_key) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Could not add SSH key: {}", err)),
|
||||
}
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
self.config_mut()
|
||||
.add_ssh_key(host, username, rsa_key)
|
||||
.map_err(|e| format!("Could not add SSH key: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,22 +32,24 @@ mod config;
|
||||
mod update;
|
||||
mod view;
|
||||
|
||||
// Deps
|
||||
extern crate crossterm;
|
||||
extern crate tuirealm;
|
||||
|
||||
// Locals
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::theme_provider::ThemeProvider;
|
||||
// Ext
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use tuirealm::{Update, View};
|
||||
|
||||
// -- components
|
||||
// -- common
|
||||
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
||||
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
|
||||
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
|
||||
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
|
||||
const COMPONENT_RADIO_SAVE: &str = "RADIO_SAVE";
|
||||
const COMPONENT_RADIO_TAB: &str = "RADIO_TAB";
|
||||
// -- config
|
||||
const COMPONENT_INPUT_TEXT_EDITOR: &str = "INPUT_TEXT_EDITOR";
|
||||
const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL";
|
||||
const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES";
|
||||
@@ -55,11 +57,51 @@ const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES";
|
||||
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
|
||||
const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT";
|
||||
const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT";
|
||||
const COMPONENT_RADIO_TAB: &str = "RADIO_TAB";
|
||||
// -- ssh keys
|
||||
const COMPONENT_LIST_SSH_KEYS: &str = "LIST_SSH_KEYS";
|
||||
const COMPONENT_INPUT_SSH_HOST: &str = "INPUT_SSH_HOST";
|
||||
const COMPONENT_INPUT_SSH_USERNAME: &str = "INPUT_SSH_USERNAME";
|
||||
const COMPONENT_RADIO_DEL_SSH_KEY: &str = "RADIO_DEL_SSH_KEY";
|
||||
// -- theme
|
||||
const COMPONENT_COLOR_AUTH_TITLE: &str = "COMPONENT_COLOR_AUTH_TITLE";
|
||||
const COMPONENT_COLOR_MISC_TITLE: &str = "COMPONENT_COLOR_MISC_TITLE";
|
||||
const COMPONENT_COLOR_TRANSFER_TITLE: &str = "COMPONENT_COLOR_TRANSFER_TITLE";
|
||||
const COMPONENT_COLOR_TRANSFER_TITLE_2: &str = "COMPONENT_COLOR_TRANSFER_TITLE_2";
|
||||
const COMPONENT_COLOR_AUTH_ADDR: &str = "COMPONENT_COLOR_AUTH_ADDR";
|
||||
const COMPONENT_COLOR_AUTH_BOOKMARKS: &str = "COMPONENT_COLOR_AUTH_BOOKMARKS";
|
||||
const COMPONENT_COLOR_AUTH_PASSWORD: &str = "COMPONENT_COLOR_AUTH_PASSWORD";
|
||||
const COMPONENT_COLOR_AUTH_PORT: &str = "COMPONENT_COLOR_AUTH_PORT";
|
||||
const COMPONENT_COLOR_AUTH_PROTOCOL: &str = "COMPONENT_COLOR_AUTH_PROTOCOL";
|
||||
const COMPONENT_COLOR_AUTH_RECENTS: &str = "COMPONENT_COLOR_AUTH_RECENTS";
|
||||
const COMPONENT_COLOR_AUTH_USERNAME: &str = "COMPONENT_COLOR_AUTH_USERNAME";
|
||||
const COMPONENT_COLOR_MISC_ERROR: &str = "COMPONENT_COLOR_MISC_ERROR";
|
||||
const COMPONENT_COLOR_MISC_INPUT: &str = "COMPONENT_COLOR_MISC_INPUT";
|
||||
const COMPONENT_COLOR_MISC_KEYS: &str = "COMPONENT_COLOR_MISC_KEYS";
|
||||
const COMPONENT_COLOR_MISC_QUIT: &str = "COMPONENT_COLOR_MISC_QUIT";
|
||||
const COMPONENT_COLOR_MISC_SAVE: &str = "COMPONENT_COLOR_MISC_SAVE";
|
||||
const COMPONENT_COLOR_MISC_WARN: &str = "COMPONENT_COLOR_MISC_WARN";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG";
|
||||
const COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG: &str =
|
||||
"COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG";
|
||||
const COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL";
|
||||
const COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL: &str = "COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL";
|
||||
const COMPONENT_COLOR_TRANSFER_LOG_BG: &str = "COMPONENT_COLOR_TRANSFER_LOG_BG";
|
||||
const COMPONENT_COLOR_TRANSFER_LOG_WIN: &str = "COMPONENT_COLOR_TRANSFER_LOG_WIN";
|
||||
const COMPONENT_COLOR_TRANSFER_STATUS_SORTING: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SORTING";
|
||||
const COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN: &str = "COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN";
|
||||
const COMPONENT_COLOR_TRANSFER_STATUS_SYNC: &str = "COMPONENT_COLOR_TRANSFER_STATUS_SYNC";
|
||||
|
||||
// -- store
|
||||
const STORE_CONFIG_CHANGED: &str = "SETUP_CONFIG_CHANGED";
|
||||
|
||||
/// ### ViewLayout
|
||||
///
|
||||
@@ -68,6 +110,7 @@ const COMPONENT_RADIO_DEL_SSH_KEY: &str = "RADIO_DEL_SSH_KEY";
|
||||
enum ViewLayout {
|
||||
SetupForm,
|
||||
SshKeys,
|
||||
Theme,
|
||||
}
|
||||
|
||||
/// ## SetupActivity
|
||||
@@ -83,11 +126,6 @@ pub struct SetupActivity {
|
||||
|
||||
impl Default for SetupActivity {
|
||||
fn default() -> Self {
|
||||
// Initialize user input
|
||||
let mut user_input_buffer: Vec<String> = Vec::with_capacity(16);
|
||||
for _ in 0..16 {
|
||||
user_input_buffer.push(String::new());
|
||||
}
|
||||
SetupActivity {
|
||||
exit_reason: None,
|
||||
context: None,
|
||||
@@ -98,6 +136,61 @@ impl Default for SetupActivity {
|
||||
}
|
||||
}
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### context
|
||||
///
|
||||
/// Returns a reference to context
|
||||
fn context(&self) -> &Context {
|
||||
self.context.as_ref().unwrap()
|
||||
}
|
||||
|
||||
/// ### context_mut
|
||||
///
|
||||
/// Returns a mutable reference to context
|
||||
fn context_mut(&mut self) -> &mut Context {
|
||||
self.context.as_mut().unwrap()
|
||||
}
|
||||
|
||||
fn config(&self) -> &ConfigClient {
|
||||
&self.context().config()
|
||||
}
|
||||
|
||||
fn config_mut(&mut self) -> &mut ConfigClient {
|
||||
self.context_mut().config_mut()
|
||||
}
|
||||
|
||||
fn theme(&self) -> &Theme {
|
||||
self.context().theme_provider().theme()
|
||||
}
|
||||
|
||||
fn theme_mut(&mut self) -> &mut Theme {
|
||||
self.context_mut().theme_provider_mut().theme_mut()
|
||||
}
|
||||
|
||||
fn theme_provider(&mut self) -> &mut ThemeProvider {
|
||||
self.context_mut().theme_provider_mut()
|
||||
}
|
||||
|
||||
/// ### config_changed
|
||||
///
|
||||
/// Returns whether config has changed
|
||||
fn config_changed(&self) -> bool {
|
||||
self.context()
|
||||
.store()
|
||||
.get_boolean(STORE_CONFIG_CHANGED)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// ### set_config_changed
|
||||
///
|
||||
/// Set value for config changed key into the store
|
||||
fn set_config_changed(&mut self, changed: bool) {
|
||||
self.context_mut()
|
||||
.store_mut()
|
||||
.set_boolean(STORE_CONFIG_CHANGED, changed);
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity for SetupActivity {
|
||||
/// ### on_create
|
||||
///
|
||||
@@ -109,14 +202,16 @@ impl Activity for SetupActivity {
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
self.context.as_mut().unwrap().clear_screen();
|
||||
// Set config changed to false
|
||||
self.set_config_changed(false);
|
||||
// Put raw mode on enabled
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Init view
|
||||
self.init_setup();
|
||||
self.init(ViewLayout::SetupForm);
|
||||
// Verify error state from context
|
||||
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||
if let Some(err) = self.context.as_mut().unwrap().error() {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
}
|
||||
@@ -131,7 +226,7 @@ impl Activity for SetupActivity {
|
||||
return;
|
||||
}
|
||||
// Read one event
|
||||
if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
|
||||
// Set redraw to true
|
||||
self.redraw = true;
|
||||
// Handle event
|
||||
|
||||
@@ -28,13 +28,26 @@
|
||||
*/
|
||||
// locals
|
||||
use super::{
|
||||
SetupActivity, COMPONENT_INPUT_LOCAL_FILE_FMT, COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||
COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR,
|
||||
COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY,
|
||||
COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES, COMPONENT_RADIO_QUIT,
|
||||
COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP,
|
||||
SetupActivity, ViewLayout, COMPONENT_COLOR_AUTH_ADDR, COMPONENT_COLOR_AUTH_BOOKMARKS,
|
||||
COMPONENT_COLOR_AUTH_PASSWORD, COMPONENT_COLOR_AUTH_PORT, COMPONENT_COLOR_AUTH_PROTOCOL,
|
||||
COMPONENT_COLOR_AUTH_RECENTS, COMPONENT_COLOR_AUTH_USERNAME, COMPONENT_COLOR_MISC_ERROR,
|
||||
COMPONENT_COLOR_MISC_INPUT, COMPONENT_COLOR_MISC_KEYS, COMPONENT_COLOR_MISC_QUIT,
|
||||
COMPONENT_COLOR_MISC_SAVE, COMPONENT_COLOR_MISC_WARN,
|
||||
COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
|
||||
COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
|
||||
COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
|
||||
COMPONENT_COLOR_TRANSFER_LOG_BG, COMPONENT_COLOR_TRANSFER_LOG_WIN,
|
||||
COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
|
||||
COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
|
||||
COMPONENT_COLOR_TRANSFER_STATUS_SYNC, COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||
COMPONENT_INPUT_REMOTE_FILE_FMT, COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME,
|
||||
COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
||||
COMPONENT_RADIO_DEL_SSH_KEY, COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES,
|
||||
COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR,
|
||||
COMPONENT_TEXT_HELP,
|
||||
};
|
||||
use crate::ui::keymap::*;
|
||||
use crate::utils::parser::parse_color;
|
||||
|
||||
// ext
|
||||
use tuirealm::{Msg, Payload, Update, Value};
|
||||
@@ -45,6 +58,16 @@ impl Update for SetupActivity {
|
||||
/// Update auth activity model based on msg
|
||||
/// The function exits when returns None
|
||||
fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
|
||||
match self.layout {
|
||||
ViewLayout::SetupForm => self.update_setup(msg),
|
||||
ViewLayout::SshKeys => self.update_ssh_keys(msg),
|
||||
ViewLayout::Theme => self.update_theme(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SetupActivity {
|
||||
fn update_setup(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
|
||||
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
|
||||
// Match msg
|
||||
match ref_msg {
|
||||
@@ -114,10 +137,11 @@ impl Update for SetupActivity {
|
||||
self.umount_error();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_ERROR, _) => None,
|
||||
// Exit
|
||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||
// Save changes
|
||||
if let Err(err) = self.action_save_config() {
|
||||
if let Err(err) = self.action_save_all() {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
// Exit
|
||||
@@ -135,12 +159,114 @@ impl Update for SetupActivity {
|
||||
self.umount_quit();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_QUIT, _) => None,
|
||||
// Close help
|
||||
(COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => {
|
||||
// Umount help
|
||||
self.umount_help();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_HELP, _) => None,
|
||||
// Save popup
|
||||
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||
// Save config
|
||||
if let Err(err) = self.action_save_all() {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
self.umount_save_popup();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => {
|
||||
// Umount radio save
|
||||
self.umount_save_popup();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_SAVE, _) => None,
|
||||
// Detect config changed
|
||||
(_, Msg::OnChange(_)) => {
|
||||
// An input field has changed value; report config changed
|
||||
self.set_config_changed(true);
|
||||
None
|
||||
}
|
||||
// <CTRL+H> Show help
|
||||
(_, &MSG_KEY_CTRL_H) => {
|
||||
// Show help
|
||||
self.mount_help();
|
||||
None
|
||||
}
|
||||
(_, &MSG_KEY_TAB) => {
|
||||
// Change view
|
||||
if let Err(err) = self.action_change_tab(ViewLayout::SshKeys) {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
None
|
||||
}
|
||||
// <CTRL+R> Revert changes
|
||||
(_, &MSG_KEY_CTRL_R) => {
|
||||
// Revert changes
|
||||
if let Err(err) = self.action_reset_config() {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
None
|
||||
}
|
||||
// <CTRL+S> Save
|
||||
(_, &MSG_KEY_CTRL_S) => {
|
||||
// Show save
|
||||
self.mount_save_popup();
|
||||
None
|
||||
}
|
||||
// <ESC>
|
||||
(_, &MSG_KEY_ESC) => {
|
||||
self.action_on_esc();
|
||||
None
|
||||
}
|
||||
(_, _) => None, // Nothing to do
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn update_ssh_keys(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
|
||||
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
|
||||
// Match msg
|
||||
match ref_msg {
|
||||
None => None,
|
||||
Some(msg) => match msg {
|
||||
// Error <ENTER> or <ESC>
|
||||
(COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) => {
|
||||
// Umount text error
|
||||
self.umount_error();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_ERROR, _) => None,
|
||||
// Exit
|
||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||
// Save changes
|
||||
if let Err(err) = self.action_save_all() {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
// Exit
|
||||
self.exit_reason = Some(super::ExitReason::Quit);
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
|
||||
// Quit
|
||||
self.exit_reason = Some(super::ExitReason::Quit);
|
||||
self.umount_quit();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => {
|
||||
// Umount popup
|
||||
self.umount_quit();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_QUIT, _) => None,
|
||||
// Close help
|
||||
(COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => {
|
||||
// Umount help
|
||||
self.umount_help();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_HELP, _) => None,
|
||||
// Delete key
|
||||
(COMPONENT_RADIO_DEL_SSH_KEY, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||
// Delete key
|
||||
@@ -156,10 +282,11 @@ impl Update for SetupActivity {
|
||||
self.umount_del_ssh_key();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_DEL_SSH_KEY, _) => None,
|
||||
// Save popup
|
||||
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||
// Save config
|
||||
if let Err(err) = self.action_save_config() {
|
||||
if let Err(err) = self.action_save_all() {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
self.umount_save_popup();
|
||||
@@ -170,13 +297,8 @@ impl Update for SetupActivity {
|
||||
self.umount_save_popup();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_SAVE, _) => None,
|
||||
// Edit SSH Key
|
||||
// <TAB> Change view
|
||||
(COMPONENT_LIST_SSH_KEYS, &MSG_KEY_TAB) => {
|
||||
// Change view
|
||||
self.init_setup();
|
||||
None
|
||||
}
|
||||
// <CTRL+H> Show help
|
||||
(_, &MSG_KEY_CTRL_H) => {
|
||||
// Show help
|
||||
@@ -242,7 +364,9 @@ impl Update for SetupActivity {
|
||||
}
|
||||
(_, &MSG_KEY_TAB) => {
|
||||
// Change view
|
||||
self.init_ssh_keys();
|
||||
if let Err(err) = self.action_change_tab(ViewLayout::Theme) {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
None
|
||||
}
|
||||
// <CTRL+R> Revert changes
|
||||
@@ -261,8 +385,326 @@ impl Update for SetupActivity {
|
||||
}
|
||||
// <ESC>
|
||||
(_, &MSG_KEY_ESC) => {
|
||||
// Mount quit prompt
|
||||
self.mount_quit();
|
||||
self.action_on_esc();
|
||||
None
|
||||
}
|
||||
(_, _) => None, // Nothing to do
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn update_theme(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
|
||||
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
|
||||
// Match msg
|
||||
match ref_msg {
|
||||
None => None,
|
||||
Some(msg) => match msg {
|
||||
// Input fields
|
||||
(COMPONENT_COLOR_AUTH_PROTOCOL, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_ADDR);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_ADDR, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_PORT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_PORT, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_USERNAME);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_USERNAME, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_PASSWORD);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_PASSWORD, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_BOOKMARKS);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_BOOKMARKS, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_RECENTS);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_RECENTS, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_ERROR);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_ERROR, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_INPUT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_INPUT, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_KEYS);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_KEYS, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_QUIT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_QUIT, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_SAVE);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_SAVE, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_WARN);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_WARN, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, &MSG_KEY_DOWN) => {
|
||||
self.view
|
||||
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, &MSG_KEY_DOWN) => {
|
||||
self.view
|
||||
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, &MSG_KEY_DOWN) => {
|
||||
self.view
|
||||
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_LOG_BG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_LOG_BG, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_LOG_WIN);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_LOG_WIN, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SORTING);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_STATUS_SORTING, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SYNC);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_STATUS_SYNC, &MSG_KEY_DOWN) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_PROTOCOL);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_PROTOCOL, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SYNC);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_ADDR, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_PROTOCOL);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_PORT, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_ADDR);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_USERNAME, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_PORT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_PASSWORD, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_USERNAME);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_BOOKMARKS, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_PASSWORD);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_AUTH_RECENTS, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_BOOKMARKS);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_ERROR, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_AUTH_RECENTS);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_INPUT, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_ERROR);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_KEYS, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_INPUT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_QUIT, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_KEYS);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_SAVE, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_QUIT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_MISC_WARN, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_SAVE);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_MISC_WARN);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, &MSG_KEY_UP) => {
|
||||
self.view
|
||||
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG, &MSG_KEY_UP) => {
|
||||
self.view
|
||||
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, &MSG_KEY_UP) => {
|
||||
self.view
|
||||
.active(COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_LOG_BG, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_LOG_WIN, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_LOG_BG);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_STATUS_SORTING, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_LOG_WIN);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_SORTING);
|
||||
None
|
||||
}
|
||||
(COMPONENT_COLOR_TRANSFER_STATUS_SYNC, &MSG_KEY_UP) => {
|
||||
self.view.active(COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN);
|
||||
None
|
||||
}
|
||||
// On color change
|
||||
(component, Msg::OnChange(Payload::One(Value::Str(color)))) => {
|
||||
if let Some(color) = parse_color(color) {
|
||||
self.action_save_color(component, color);
|
||||
// Set unsaved changes to true
|
||||
self.set_config_changed(true);
|
||||
}
|
||||
None
|
||||
}
|
||||
// Error <ENTER> or <ESC>
|
||||
(COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) => {
|
||||
// Umount text error
|
||||
self.umount_error();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_ERROR, _) => None,
|
||||
// Exit
|
||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||
// Save changes
|
||||
if let Err(err) = self.action_save_all() {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
// Exit
|
||||
self.exit_reason = Some(super::ExitReason::Quit);
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
|
||||
// Quit
|
||||
self.exit_reason = Some(super::ExitReason::Quit);
|
||||
self.umount_quit();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(_)) => {
|
||||
// Umount popup
|
||||
self.umount_quit();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_QUIT, _) => None,
|
||||
// Close help
|
||||
(COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) | (COMPONENT_TEXT_HELP, &MSG_KEY_ESC) => {
|
||||
// Umount help
|
||||
self.umount_help();
|
||||
None
|
||||
}
|
||||
(COMPONENT_TEXT_HELP, _) => None,
|
||||
// Save popup
|
||||
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||
// Save config
|
||||
if let Err(err) = self.action_save_all() {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
self.umount_save_popup();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(_)) => {
|
||||
// Umount radio save
|
||||
self.umount_save_popup();
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_SAVE, _) => None,
|
||||
// Edit SSH Key
|
||||
// <CTRL+H> Show help
|
||||
(_, &MSG_KEY_CTRL_H) => {
|
||||
// Show help
|
||||
self.mount_help();
|
||||
None
|
||||
}
|
||||
(_, &MSG_KEY_TAB) => {
|
||||
// Change view
|
||||
if let Err(err) = self.action_change_tab(ViewLayout::SetupForm) {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
None
|
||||
}
|
||||
// <CTRL+R> Revert changes
|
||||
(_, &MSG_KEY_CTRL_R) => {
|
||||
// Revert changes
|
||||
if let Err(err) = self.action_reset_theme() {
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
None
|
||||
}
|
||||
// <CTRL+S> Save
|
||||
(_, &MSG_KEY_CTRL_S) => {
|
||||
// Show save
|
||||
self.mount_save_popup();
|
||||
None
|
||||
}
|
||||
// <ESC>
|
||||
(_, &MSG_KEY_ESC) => {
|
||||
self.action_on_esc();
|
||||
None
|
||||
}
|
||||
(_, _) => None, // Nothing to do
|
||||
|
||||
@@ -1,808 +0,0 @@
|
||||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/**
|
||||
* 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 super::{Context, SetupActivity, ViewLayout};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs;
|
||||
use crate::ui::components::{
|
||||
bookmark_list::{BookmarkList, BookmarkListPropsBuilder},
|
||||
msgbox::{MsgBox, MsgBoxPropsBuilder},
|
||||
};
|
||||
use crate::utils::ui::draw_area_in;
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
use tuirealm::components::{
|
||||
input::{Input, InputPropsBuilder},
|
||||
radio::{Radio, RadioPropsBuilder},
|
||||
scrolltable::{ScrollTablePropsBuilder, Scrolltable},
|
||||
span::{Span, SpanPropsBuilder},
|
||||
};
|
||||
use tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders, Clear},
|
||||
};
|
||||
use tuirealm::{
|
||||
props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
|
||||
Payload, Value, View,
|
||||
};
|
||||
|
||||
impl SetupActivity {
|
||||
// -- view
|
||||
|
||||
/// ### init_setup
|
||||
///
|
||||
/// Initialize setup view
|
||||
pub(super) fn init_setup(&mut self) {
|
||||
// Init view
|
||||
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")],
|
||||
)
|
||||
.with_value(0)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// 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(),
|
||||
)),
|
||||
);
|
||||
// Input fields
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_TEXT_EDITOR,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightGreen)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||
.with_label(String::from("Text editor"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_HIDDEN_FILES,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_UPDATES,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_GROUP_DIRS,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
|
||||
.with_label(String::from("File formatter syntax (local)"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightGreen)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||
.with_label(String::from("File formatter syntax (remote)"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Load values
|
||||
self.load_input_values();
|
||||
// Set view
|
||||
self.layout = ViewLayout::SetupForm;
|
||||
}
|
||||
|
||||
/// ### init_ssh_keys
|
||||
///
|
||||
/// Initialize ssh keys view
|
||||
pub(super) fn init_ssh_keys(&mut self) {
|
||||
// Init view
|
||||
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")],
|
||||
)
|
||||
.with_value(1)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// 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.view.mount(
|
||||
super::COMPONENT_LIST_SSH_KEYS,
|
||||
Box::new(BookmarkList::new(
|
||||
BookmarkListPropsBuilder::default()
|
||||
.with_bookmarks(Some(String::from("SSH Keys")), vec![])
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||
.with_background(Color::LightGreen)
|
||||
.with_foreground(Color::Black)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Give focus
|
||||
self.view.active(super::COMPONENT_LIST_SSH_KEYS);
|
||||
// Load keys
|
||||
self.reload_ssh_keys();
|
||||
// Set view
|
||||
self.layout = ViewLayout::SshKeys;
|
||||
}
|
||||
|
||||
/// ### view
|
||||
///
|
||||
/// View gui
|
||||
pub(super) fn view(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal.draw(|f| {
|
||||
// Prepare main chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Current tab
|
||||
Constraint::Percentage(90), // Main body
|
||||
Constraint::Length(3), // Help footer
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// Render common widget
|
||||
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
|
||||
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
|
||||
match self.layout {
|
||||
ViewLayout::SetupForm => {
|
||||
// Make chunks
|
||||
let ui_cfg_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Text editor
|
||||
Constraint::Length(3), // Protocol tab
|
||||
Constraint::Length(3), // Hidden files
|
||||
Constraint::Length(3), // Updates tab
|
||||
Constraint::Length(3), // Group dirs
|
||||
Constraint::Length(3), // Local Format input
|
||||
Constraint::Length(3), // Remote Format input
|
||||
Constraint::Length(1), // Empty ?
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(chunks[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, f, ui_cfg_chunks[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[4]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[5]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[6]);
|
||||
}
|
||||
ViewLayout::SshKeys => {
|
||||
let sshcfg_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.split(chunks[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_LIST_SSH_KEYS, f, sshcfg_chunks[0]);
|
||||
}
|
||||
}
|
||||
// Popups
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 70);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 20);
|
||||
f.render_widget(Clear, popup);
|
||||
let popup_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Host
|
||||
Constraint::Length(3), // Username
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_SSH_HOST, f, popup_chunks[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_SSH_USERNAME, f, popup_chunks[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Put context back to context
|
||||
self.context = Some(ctx);
|
||||
}
|
||||
|
||||
// -- mount
|
||||
|
||||
/// ### mount_error
|
||||
///
|
||||
/// Mount error box
|
||||
pub(super) fn mount_error(&mut self, text: &str) {
|
||||
// Mount
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_ERROR,
|
||||
Box::new(MsgBox::new(
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.bold()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Give focus to error
|
||||
self.view.active(super::COMPONENT_TEXT_ERROR);
|
||||
}
|
||||
|
||||
/// ### umount_error
|
||||
///
|
||||
/// Umount error message
|
||||
pub(super) fn umount_error(&mut self) {
|
||||
self.view.umount(super::COMPONENT_TEXT_ERROR);
|
||||
}
|
||||
|
||||
/// ### mount_del_ssh_key
|
||||
///
|
||||
/// Mount delete ssh key component
|
||||
pub(super) fn mount_del_ssh_key(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_DEL_SSH_KEY,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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_value(1) // Default: No
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Active
|
||||
self.view.active(super::COMPONENT_RADIO_DEL_SSH_KEY);
|
||||
}
|
||||
|
||||
/// ### umount_del_ssh_key
|
||||
///
|
||||
/// Umount delete ssh key
|
||||
pub(super) fn umount_del_ssh_key(&mut self) {
|
||||
self.view.umount(super::COMPONENT_RADIO_DEL_SSH_KEY);
|
||||
}
|
||||
|
||||
/// ### mount_new_ssh_key
|
||||
///
|
||||
/// Mount new ssh key prompt
|
||||
pub(super) fn mount_new_ssh_key(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_SSH_HOST,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_label(String::from("Hostname or address"))
|
||||
.with_borders(
|
||||
Borders::TOP | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Plain,
|
||||
Color::Reset,
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_SSH_USERNAME,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_label(String::from("Username"))
|
||||
.with_borders(
|
||||
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Plain,
|
||||
Color::Reset,
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.active(super::COMPONENT_INPUT_SSH_HOST);
|
||||
}
|
||||
|
||||
/// ### umount_new_ssh_key
|
||||
///
|
||||
/// Umount new ssh key prompt
|
||||
pub(super) fn umount_new_ssh_key(&mut self) {
|
||||
self.view.umount(super::COMPONENT_INPUT_SSH_HOST);
|
||||
self.view.umount(super::COMPONENT_INPUT_SSH_USERNAME);
|
||||
}
|
||||
|
||||
/// ### mount_quit
|
||||
///
|
||||
/// Mount quit popup
|
||||
pub(super) fn mount_quit(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_QUIT,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.with_color(Color::LightRed)
|
||||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
|
||||
.with_options(
|
||||
Some(String::from("Exit setup?")),
|
||||
vec![
|
||||
String::from("Save"),
|
||||
String::from("Don't save"),
|
||||
String::from("Cancel"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Active
|
||||
self.view.active(super::COMPONENT_RADIO_QUIT);
|
||||
}
|
||||
|
||||
/// ### umount_quit
|
||||
///
|
||||
/// Umount quit
|
||||
pub(super) fn umount_quit(&mut self) {
|
||||
self.view.umount(super::COMPONENT_RADIO_QUIT);
|
||||
}
|
||||
|
||||
/// ### mount_save_popup
|
||||
///
|
||||
/// Mount save popup
|
||||
pub(super) fn mount_save_popup(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_SAVE,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Active
|
||||
self.view.active(super::COMPONENT_RADIO_SAVE);
|
||||
}
|
||||
|
||||
/// ### umount_quit
|
||||
///
|
||||
/// Umount quit
|
||||
pub(super) fn umount_save_popup(&mut self) {
|
||||
self.view.umount(super::COMPONENT_RADIO_SAVE);
|
||||
}
|
||||
|
||||
/// ### mount_help
|
||||
///
|
||||
/// Mount help
|
||||
pub(super) fn mount_help(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_HELP,
|
||||
Box::new(Scrolltable::new(
|
||||
ScrollTablePropsBuilder::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")),
|
||||
TableBuilder::default()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<ESC>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Exit setup"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<TAB>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Change setup page"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<RIGHT/LEFT>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Change cursor"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<UP/DOWN>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Change input field"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<ENTER>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Select / Dismiss popup"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<DEL|E>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Delete SSH key"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<CTRL+N>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" New SSH key"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<CTRL+R>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Revert changes"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<CTRL+S>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Save configuration"))
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Active help
|
||||
self.view.active(super::COMPONENT_TEXT_HELP);
|
||||
}
|
||||
|
||||
/// ### umount_help
|
||||
///
|
||||
/// Umount help
|
||||
pub(super) fn umount_help(&mut self) {
|
||||
self.view.umount(super::COMPONENT_TEXT_HELP);
|
||||
}
|
||||
|
||||
/// ### load_input_values
|
||||
///
|
||||
/// Load values from configuration into input fields
|
||||
pub(super) fn load_input_values(&mut self) {
|
||||
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
// Text editor
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) {
|
||||
let text_editor: String =
|
||||
String::from(cli.get_text_editor().as_path().to_string_lossy());
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(text_editor)
|
||||
.build();
|
||||
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
|
||||
}
|
||||
// Protocol
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) {
|
||||
let protocol: usize = match cli.get_default_protocol() {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(false) => 2,
|
||||
FileTransferProtocol::Ftp(true) => 3,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
|
||||
}
|
||||
// Hidden files
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) {
|
||||
let hidden: usize = match cli.get_show_hidden_files() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(hidden).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
|
||||
}
|
||||
// Updates
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) {
|
||||
let updates: usize = match cli.get_check_for_updates() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(updates).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
|
||||
}
|
||||
// Group dirs
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
|
||||
let dirs: usize = match cli.get_group_dirs() {
|
||||
Some(GroupDirs::First) => 0,
|
||||
Some(GroupDirs::Last) => 1,
|
||||
None => 2,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(dirs).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
|
||||
}
|
||||
// Local File Fmt
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_LOCAL_FILE_FMT) {
|
||||
let file_fmt: String = cli.get_local_file_fmt().unwrap_or_default();
|
||||
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_INPUT_LOCAL_FILE_FMT, props);
|
||||
}
|
||||
// Remote File Fmt
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_REMOTE_FILE_FMT) {
|
||||
let file_fmt: String = cli.get_remote_file_fmt().unwrap_or_default();
|
||||
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### collect_input_values
|
||||
///
|
||||
/// Collect values from input and put them into the configuration
|
||||
pub(super) fn collect_input_values(&mut self) {
|
||||
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||
if let Some(Payload::One(Value::Str(editor))) =
|
||||
self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR)
|
||||
{
|
||||
cli.set_text_editor(PathBuf::from(editor.as_str()));
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(protocol))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
|
||||
{
|
||||
let protocol: FileTransferProtocol = match protocol {
|
||||
1 => FileTransferProtocol::Scp,
|
||||
2 => FileTransferProtocol::Ftp(false),
|
||||
3 => FileTransferProtocol::Ftp(true),
|
||||
_ => FileTransferProtocol::Sftp,
|
||||
};
|
||||
cli.set_default_protocol(protocol);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES)
|
||||
{
|
||||
let show: bool = matches!(opt, 0);
|
||||
cli.set_show_hidden_files(show);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_UPDATES)
|
||||
{
|
||||
let check: bool = matches!(opt, 0);
|
||||
cli.set_check_for_updates(check);
|
||||
}
|
||||
if let Some(Payload::One(Value::Str(fmt))) =
|
||||
self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT)
|
||||
{
|
||||
cli.set_local_file_fmt(fmt);
|
||||
}
|
||||
if let Some(Payload::One(Value::Str(fmt))) =
|
||||
self.view.get_state(super::COMPONENT_INPUT_REMOTE_FILE_FMT)
|
||||
{
|
||||
cli.set_remote_file_fmt(fmt);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS)
|
||||
{
|
||||
let dirs: Option<GroupDirs> = match opt {
|
||||
0 => Some(GroupDirs::First),
|
||||
1 => Some(GroupDirs::Last),
|
||||
_ => None,
|
||||
};
|
||||
cli.set_group_dirs(dirs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### reload_ssh_keys
|
||||
///
|
||||
/// Reload ssh keys
|
||||
pub(super) fn reload_ssh_keys(&mut self) {
|
||||
if let Some(cli) = self.context.as_ref().unwrap().config_client.as_ref() {
|
||||
// get props
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) {
|
||||
// Create texts
|
||||
let keys: Vec<String> = cli
|
||||
.iter_ssh_keys()
|
||||
.map(|x| {
|
||||
let (addr, username, _) = cli.get_ssh_key(x).ok().unwrap().unwrap();
|
||||
format!("{} at {}", addr, username)
|
||||
})
|
||||
.collect();
|
||||
let props = BookmarkListPropsBuilder::from(props)
|
||||
.with_bookmarks(Some(String::from("SSH Keys")), keys)
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
267
src/ui/activities/setup/view/mod.rs
Normal file
267
src/ui/activities/setup/view/mod.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
pub mod setup;
|
||||
pub mod ssh_keys;
|
||||
pub mod theme;
|
||||
|
||||
use super::*;
|
||||
pub use setup::*;
|
||||
pub use ssh_keys::*;
|
||||
pub use theme::*;
|
||||
// Locals
|
||||
use crate::ui::components::msgbox::{MsgBox, MsgBoxPropsBuilder};
|
||||
// Ext
|
||||
use tuirealm::components::{
|
||||
radio::{Radio, RadioPropsBuilder},
|
||||
scrolltable::{ScrollTablePropsBuilder, Scrolltable},
|
||||
};
|
||||
use tuirealm::props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder};
|
||||
use tuirealm::tui::{
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders},
|
||||
};
|
||||
|
||||
impl SetupActivity {
|
||||
// -- view
|
||||
|
||||
pub(super) fn init(&mut self, layout: ViewLayout) {
|
||||
self.layout = layout;
|
||||
match self.layout {
|
||||
ViewLayout::SetupForm => self.init_setup(),
|
||||
ViewLayout::SshKeys => self.init_ssh_keys(),
|
||||
ViewLayout::Theme => self.init_theme(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### view
|
||||
///
|
||||
/// View gui
|
||||
pub(super) fn view(&mut self) {
|
||||
match self.layout {
|
||||
ViewLayout::SetupForm => self.view_setup(),
|
||||
ViewLayout::SshKeys => self.view_ssh_keys(),
|
||||
ViewLayout::Theme => self.view_theme(),
|
||||
}
|
||||
}
|
||||
|
||||
// -- mount
|
||||
|
||||
/// ### mount_error
|
||||
///
|
||||
/// Mount error box
|
||||
pub(super) fn mount_error(&mut self, text: &str) {
|
||||
// Mount
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_ERROR,
|
||||
Box::new(MsgBox::new(
|
||||
MsgBoxPropsBuilder::default()
|
||||
.with_foreground(Color::Red)
|
||||
.bold()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
|
||||
.with_texts(None, vec![TextSpan::from(text)])
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Give focus to error
|
||||
self.view.active(super::COMPONENT_TEXT_ERROR);
|
||||
}
|
||||
|
||||
/// ### umount_error
|
||||
///
|
||||
/// Umount error message
|
||||
pub(super) fn umount_error(&mut self) {
|
||||
self.view.umount(super::COMPONENT_TEXT_ERROR);
|
||||
}
|
||||
|
||||
/// ### mount_quit
|
||||
///
|
||||
/// Mount quit popup
|
||||
pub(super) fn mount_quit(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_QUIT,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Active
|
||||
self.view.active(super::COMPONENT_RADIO_QUIT);
|
||||
}
|
||||
|
||||
/// ### umount_quit
|
||||
///
|
||||
/// Umount quit
|
||||
pub(super) fn umount_quit(&mut self) {
|
||||
self.view.umount(super::COMPONENT_RADIO_QUIT);
|
||||
}
|
||||
|
||||
/// ### mount_save_popup
|
||||
///
|
||||
/// Mount save popup
|
||||
pub(super) fn mount_save_popup(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_SAVE,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Active
|
||||
self.view.active(super::COMPONENT_RADIO_SAVE);
|
||||
}
|
||||
|
||||
/// ### umount_quit
|
||||
///
|
||||
/// Umount quit
|
||||
pub(super) fn umount_save_popup(&mut self) {
|
||||
self.view.umount(super::COMPONENT_RADIO_SAVE);
|
||||
}
|
||||
|
||||
/// ### mount_help
|
||||
///
|
||||
/// Mount help
|
||||
pub(super) fn mount_help(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_TEXT_HELP,
|
||||
Box::new(Scrolltable::new(
|
||||
ScrollTablePropsBuilder::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")),
|
||||
TableBuilder::default()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<ESC>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Exit setup"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<TAB>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Change setup page"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<RIGHT/LEFT>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Change cursor"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<UP/DOWN>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Change input field"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<ENTER>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Select / Dismiss popup"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<DEL|E>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Delete SSH key"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<CTRL+N>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" New SSH key"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<CTRL+R>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Revert changes"))
|
||||
.add_row()
|
||||
.add_col(
|
||||
TextSpanBuilder::new("<CTRL+S>")
|
||||
.bold()
|
||||
.with_foreground(Color::Cyan)
|
||||
.build(),
|
||||
)
|
||||
.add_col(TextSpan::from(" Save configuration"))
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Active help
|
||||
self.view.active(super::COMPONENT_TEXT_HELP);
|
||||
}
|
||||
|
||||
/// ### umount_help
|
||||
///
|
||||
/// Umount help
|
||||
pub(super) fn umount_help(&mut self) {
|
||||
self.view.umount(super::COMPONENT_TEXT_HELP);
|
||||
}
|
||||
}
|
||||
411
src/ui/activities/setup/view/setup.rs
Normal file
411
src/ui/activities/setup/view/setup.rs
Normal file
@@ -0,0 +1,411 @@
|
||||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/**
|
||||
* 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 super::{Context, SetupActivity};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs;
|
||||
use crate::utils::ui::draw_area_in;
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
use tuirealm::components::{
|
||||
input::{Input, InputPropsBuilder},
|
||||
radio::{Radio, RadioPropsBuilder},
|
||||
span::{Span, SpanPropsBuilder},
|
||||
};
|
||||
use tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders, Clear},
|
||||
};
|
||||
use tuirealm::{
|
||||
props::{PropsBuilder, TextSpanBuilder},
|
||||
Payload, Value, View,
|
||||
};
|
||||
|
||||
impl SetupActivity {
|
||||
// -- view
|
||||
|
||||
/// ### init_setup
|
||||
///
|
||||
/// Initialize setup view
|
||||
pub(super) fn init_setup(&mut self) {
|
||||
// Init view
|
||||
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(),
|
||||
)),
|
||||
);
|
||||
// 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(),
|
||||
)),
|
||||
);
|
||||
// Input fields
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_TEXT_EDITOR,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightGreen)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||
.with_label(String::from("Text editor"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_HIDDEN_FILES,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_UPDATES,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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")],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_GROUP_DIRS,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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"),
|
||||
],
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightBlue)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
|
||||
.with_label(String::from("File formatter syntax (local)"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(Color::LightGreen)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||
.with_label(String::from("File formatter syntax (remote)"))
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Load values
|
||||
self.load_input_values();
|
||||
}
|
||||
|
||||
pub(super) fn view_setup(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal().draw(|f| {
|
||||
// Prepare main chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Current tab
|
||||
Constraint::Length(21), // Main body
|
||||
Constraint::Length(3), // Help footer
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// Render common widget
|
||||
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
|
||||
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
|
||||
// Make chunks
|
||||
let ui_cfg_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Text editor
|
||||
Constraint::Length(3), // Protocol tab
|
||||
Constraint::Length(3), // Hidden files
|
||||
Constraint::Length(3), // Updates tab
|
||||
Constraint::Length(3), // Group dirs
|
||||
Constraint::Length(3), // Local Format input
|
||||
Constraint::Length(3), // Remote Format input
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(chunks[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, f, ui_cfg_chunks[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[4]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[5]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[6]);
|
||||
// Popups
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 70);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Put context back to context
|
||||
self.context = Some(ctx);
|
||||
}
|
||||
|
||||
/// ### load_input_values
|
||||
///
|
||||
/// Load values from configuration into input fields
|
||||
pub(crate) fn load_input_values(&mut self) {
|
||||
// Text editor
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) {
|
||||
let text_editor: String =
|
||||
String::from(self.config().get_text_editor().as_path().to_string_lossy());
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(text_editor)
|
||||
.build();
|
||||
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
|
||||
}
|
||||
// Protocol
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) {
|
||||
let protocol: usize = match self.config().get_default_protocol() {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(false) => 2,
|
||||
FileTransferProtocol::Ftp(true) => 3,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
|
||||
}
|
||||
// Hidden files
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) {
|
||||
let hidden: usize = match self.config().get_show_hidden_files() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(hidden).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
|
||||
}
|
||||
// Updates
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) {
|
||||
let updates: usize = match self.config().get_check_for_updates() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(updates).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
|
||||
}
|
||||
// Group dirs
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
|
||||
let dirs: usize = match self.config().get_group_dirs() {
|
||||
Some(GroupDirs::First) => 0,
|
||||
Some(GroupDirs::Last) => 1,
|
||||
None => 2,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(dirs).build();
|
||||
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
|
||||
}
|
||||
// Local File Fmt
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_LOCAL_FILE_FMT) {
|
||||
let file_fmt: String = self.config().get_local_file_fmt().unwrap_or_default();
|
||||
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_INPUT_LOCAL_FILE_FMT, props);
|
||||
}
|
||||
// Remote File Fmt
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_REMOTE_FILE_FMT) {
|
||||
let file_fmt: String = self.config().get_remote_file_fmt().unwrap_or_default();
|
||||
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
|
||||
let _ = self
|
||||
.view
|
||||
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### collect_input_values
|
||||
///
|
||||
/// Collect values from input and put them into the configuration
|
||||
pub(crate) fn collect_input_values(&mut self) {
|
||||
if let Some(Payload::One(Value::Str(editor))) =
|
||||
self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR)
|
||||
{
|
||||
self.config_mut()
|
||||
.set_text_editor(PathBuf::from(editor.as_str()));
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(protocol))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
|
||||
{
|
||||
let protocol: FileTransferProtocol = match protocol {
|
||||
1 => FileTransferProtocol::Scp,
|
||||
2 => FileTransferProtocol::Ftp(false),
|
||||
3 => FileTransferProtocol::Ftp(true),
|
||||
_ => FileTransferProtocol::Sftp,
|
||||
};
|
||||
self.config_mut().set_default_protocol(protocol);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES)
|
||||
{
|
||||
let show: bool = matches!(opt, 0);
|
||||
self.config_mut().set_show_hidden_files(show);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_UPDATES)
|
||||
{
|
||||
let check: bool = matches!(opt, 0);
|
||||
self.config_mut().set_check_for_updates(check);
|
||||
}
|
||||
if let Some(Payload::One(Value::Str(fmt))) =
|
||||
self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT)
|
||||
{
|
||||
self.config_mut().set_local_file_fmt(fmt);
|
||||
}
|
||||
if let Some(Payload::One(Value::Str(fmt))) =
|
||||
self.view.get_state(super::COMPONENT_INPUT_REMOTE_FILE_FMT)
|
||||
{
|
||||
self.config_mut().set_remote_file_fmt(fmt);
|
||||
}
|
||||
if let Some(Payload::One(Value::Usize(opt))) =
|
||||
self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS)
|
||||
{
|
||||
let dirs: Option<GroupDirs> = match opt {
|
||||
0 => Some(GroupDirs::First),
|
||||
1 => Some(GroupDirs::Last),
|
||||
_ => None,
|
||||
};
|
||||
self.config_mut().set_group_dirs(dirs);
|
||||
}
|
||||
}
|
||||
}
|
||||
295
src/ui/activities/setup/view/ssh_keys.rs
Normal file
295
src/ui/activities/setup/view/ssh_keys.rs
Normal file
@@ -0,0 +1,295 @@
|
||||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/**
|
||||
* 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 super::{Context, SetupActivity};
|
||||
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
|
||||
use crate::utils::ui::draw_area_in;
|
||||
// Ext
|
||||
use tuirealm::components::{
|
||||
input::{Input, InputPropsBuilder},
|
||||
radio::{Radio, RadioPropsBuilder},
|
||||
span::{Span, SpanPropsBuilder},
|
||||
};
|
||||
use tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders, Clear},
|
||||
};
|
||||
use tuirealm::{
|
||||
props::{PropsBuilder, TextSpanBuilder},
|
||||
View,
|
||||
};
|
||||
|
||||
impl SetupActivity {
|
||||
// -- view
|
||||
|
||||
/// ### init_ssh_keys
|
||||
///
|
||||
/// Initialize ssh keys view
|
||||
pub(super) fn init_ssh_keys(&mut self) {
|
||||
// Init view
|
||||
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(),
|
||||
)),
|
||||
);
|
||||
// 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.view.mount(
|
||||
super::COMPONENT_LIST_SSH_KEYS,
|
||||
Box::new(BookmarkList::new(
|
||||
BookmarkListPropsBuilder::default()
|
||||
.with_bookmarks(Some(String::from("SSH Keys")), vec![])
|
||||
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||
.with_background(Color::LightGreen)
|
||||
.with_foreground(Color::Black)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Give focus
|
||||
self.view.active(super::COMPONENT_LIST_SSH_KEYS);
|
||||
// Load keys
|
||||
self.reload_ssh_keys();
|
||||
}
|
||||
|
||||
pub(crate) fn view_ssh_keys(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal().draw(|f| {
|
||||
// Prepare main chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Current tab
|
||||
Constraint::Percentage(90), // Main body
|
||||
Constraint::Length(3), // Help footer
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// Render common widget
|
||||
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
|
||||
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_LIST_SSH_KEYS, f, chunks[1]);
|
||||
// Popups
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 70);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 20);
|
||||
f.render_widget(Clear, popup);
|
||||
let popup_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Host
|
||||
Constraint::Length(3), // Username
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_SSH_HOST, f, popup_chunks[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_SSH_USERNAME, f, popup_chunks[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Put context back to context
|
||||
self.context = Some(ctx);
|
||||
}
|
||||
|
||||
// -- mount
|
||||
|
||||
/// ### mount_del_ssh_key
|
||||
///
|
||||
/// Mount delete ssh key component
|
||||
pub(crate) fn mount_del_ssh_key(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_RADIO_DEL_SSH_KEY,
|
||||
Box::new(Radio::new(
|
||||
RadioPropsBuilder::default()
|
||||
.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_value(1) // Default: No
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Active
|
||||
self.view.active(super::COMPONENT_RADIO_DEL_SSH_KEY);
|
||||
}
|
||||
|
||||
/// ### umount_del_ssh_key
|
||||
///
|
||||
/// Umount delete ssh key
|
||||
pub(crate) fn umount_del_ssh_key(&mut self) {
|
||||
self.view.umount(super::COMPONENT_RADIO_DEL_SSH_KEY);
|
||||
}
|
||||
|
||||
/// ### mount_new_ssh_key
|
||||
///
|
||||
/// Mount new ssh key prompt
|
||||
pub(crate) fn mount_new_ssh_key(&mut self) {
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_SSH_HOST,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_label(String::from("Hostname or address"))
|
||||
.with_borders(
|
||||
Borders::TOP | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Plain,
|
||||
Color::Reset,
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_SSH_USERNAME,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_label(String::from("Username"))
|
||||
.with_borders(
|
||||
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
|
||||
BorderType::Plain,
|
||||
Color::Reset,
|
||||
)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
self.view.active(super::COMPONENT_INPUT_SSH_HOST);
|
||||
}
|
||||
|
||||
/// ### umount_new_ssh_key
|
||||
///
|
||||
/// Umount new ssh key prompt
|
||||
pub(crate) fn umount_new_ssh_key(&mut self) {
|
||||
self.view.umount(super::COMPONENT_INPUT_SSH_HOST);
|
||||
self.view.umount(super::COMPONENT_INPUT_SSH_USERNAME);
|
||||
}
|
||||
|
||||
/// ### reload_ssh_keys
|
||||
///
|
||||
/// Reload ssh keys
|
||||
pub(crate) fn reload_ssh_keys(&mut self) {
|
||||
// get props
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) {
|
||||
// Create texts
|
||||
let keys: Vec<String> = self
|
||||
.config()
|
||||
.iter_ssh_keys()
|
||||
.map(|x| {
|
||||
let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap();
|
||||
format!("{} at {}", addr, username)
|
||||
})
|
||||
.collect();
|
||||
let props = BookmarkListPropsBuilder::from(props)
|
||||
.with_bookmarks(Some(String::from("SSH Keys")), keys)
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
|
||||
}
|
||||
}
|
||||
}
|
||||
676
src/ui/activities/setup/view/theme.rs
Normal file
676
src/ui/activities/setup/view/theme.rs
Normal file
@@ -0,0 +1,676 @@
|
||||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/**
|
||||
* 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 super::{Context, SetupActivity};
|
||||
use crate::config::themes::Theme;
|
||||
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 tuirealm::tui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders, Clear},
|
||||
};
|
||||
use tuirealm::{
|
||||
props::{PropsBuilder, TextSpanBuilder},
|
||||
Payload, Value, View,
|
||||
};
|
||||
|
||||
impl SetupActivity {
|
||||
// -- view
|
||||
|
||||
/// ### init_theme
|
||||
///
|
||||
/// Initialize thene view
|
||||
pub(super) fn init_theme(&mut self) {
|
||||
// Init view
|
||||
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(),
|
||||
)),
|
||||
);
|
||||
// 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(),
|
||||
)),
|
||||
);
|
||||
// auth colors
|
||||
self.mount_title(super::COMPONENT_COLOR_AUTH_TITLE, "Authentication styles");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PROTOCOL, "Protocol");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_ADDR, "Ip address");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PORT, "Port");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_USERNAME, "Username");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_PASSWORD, "Password");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_BOOKMARKS, "Bookmarks");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_AUTH_RECENTS, "Recent connections");
|
||||
// Misc
|
||||
self.mount_title(super::COMPONENT_COLOR_MISC_TITLE, "Misc styles");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_ERROR, "Error");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_INPUT, "Input fields");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_KEYS, "Key strokes");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_QUIT, "Quit dialogs");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_SAVE, "Save confirmations");
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_MISC_WARN, "Warnings");
|
||||
// Transfer (1)
|
||||
self.mount_title(super::COMPONENT_COLOR_TRANSFER_TITLE, "Transfer styles");
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
|
||||
"Local explorer background",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
|
||||
"Local explorer foreground",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
|
||||
"Local explorer highlighted",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
|
||||
"Remote explorer background",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
|
||||
"Remote explorer foreground",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
|
||||
"Remote explorer highlighted",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
|
||||
"'Full transfer' Progress bar",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
|
||||
"'Partial transfer' Progress bar",
|
||||
);
|
||||
// Transfer (2)
|
||||
self.mount_title(
|
||||
super::COMPONENT_COLOR_TRANSFER_TITLE_2,
|
||||
"Transfer styles (2)",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
|
||||
"Log window background",
|
||||
);
|
||||
self.mount_color_picker(super::COMPONENT_COLOR_TRANSFER_LOG_WIN, "Log window");
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
|
||||
"File sorting",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
|
||||
"Hidden files",
|
||||
);
|
||||
self.mount_color_picker(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
|
||||
"Synchronized browsing",
|
||||
);
|
||||
// Load styles
|
||||
self.load_styles();
|
||||
// Active first field
|
||||
self.view.active(super::COMPONENT_COLOR_AUTH_PROTOCOL);
|
||||
}
|
||||
|
||||
pub(super) fn view_theme(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal().draw(|f| {
|
||||
// Prepare main chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Current tab
|
||||
Constraint::Length(22), // Main body
|
||||
Constraint::Length(3), // Help footer
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// Render common widget
|
||||
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
|
||||
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
|
||||
// Make chunks
|
||||
let colors_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(chunks[1]);
|
||||
let auth_colors_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1), // Title
|
||||
Constraint::Length(3), // Protocol
|
||||
Constraint::Length(3), // Addr
|
||||
Constraint::Length(3), // Port
|
||||
Constraint::Length(3), // Username
|
||||
Constraint::Length(3), // Password
|
||||
Constraint::Length(3), // Bookmarks
|
||||
Constraint::Length(3), // Recents
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(colors_layout[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_AUTH_TITLE, f, auth_colors_layout[0]);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_AUTH_PROTOCOL,
|
||||
f,
|
||||
auth_colors_layout[1],
|
||||
);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_AUTH_ADDR, f, auth_colors_layout[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_AUTH_PORT, f, auth_colors_layout[3]);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_AUTH_USERNAME,
|
||||
f,
|
||||
auth_colors_layout[4],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_AUTH_PASSWORD,
|
||||
f,
|
||||
auth_colors_layout[5],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_AUTH_BOOKMARKS,
|
||||
f,
|
||||
auth_colors_layout[6],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_AUTH_RECENTS,
|
||||
f,
|
||||
auth_colors_layout[7],
|
||||
);
|
||||
let misc_colors_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1), // Title
|
||||
Constraint::Length(3), // Error
|
||||
Constraint::Length(3), // Input
|
||||
Constraint::Length(3), // Keys
|
||||
Constraint::Length(3), // Quit
|
||||
Constraint::Length(3), // Save
|
||||
Constraint::Length(3), // Warn
|
||||
Constraint::Length(3), // Empty
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(colors_layout[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_TITLE, f, misc_colors_layout[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_ERROR, f, misc_colors_layout[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_INPUT, f, misc_colors_layout[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_KEYS, f, misc_colors_layout[3]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_QUIT, f, misc_colors_layout[4]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_SAVE, f, misc_colors_layout[5]);
|
||||
self.view
|
||||
.render(super::COMPONENT_COLOR_MISC_WARN, f, misc_colors_layout[6]);
|
||||
|
||||
let transfer_colors_layout_col1 = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1), // Title
|
||||
Constraint::Length(3), // local explorer bg
|
||||
Constraint::Length(3), // local explorer fg
|
||||
Constraint::Length(3), // local explorer hg
|
||||
Constraint::Length(3), // remote explorer bg
|
||||
Constraint::Length(3), // remote explorer fg
|
||||
Constraint::Length(3), // remote explorer hg
|
||||
Constraint::Length(3), // empty
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(colors_layout[2]);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_TITLE,
|
||||
f,
|
||||
transfer_colors_layout_col1[0],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
|
||||
f,
|
||||
transfer_colors_layout_col1[1],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
|
||||
f,
|
||||
transfer_colors_layout_col1[2],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
|
||||
f,
|
||||
transfer_colors_layout_col1[3],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
|
||||
f,
|
||||
transfer_colors_layout_col1[4],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
|
||||
f,
|
||||
transfer_colors_layout_col1[5],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
|
||||
f,
|
||||
transfer_colors_layout_col1[6],
|
||||
);
|
||||
let transfer_colors_layout_col2 = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1), // Title
|
||||
Constraint::Length(3), // Full prog bar
|
||||
Constraint::Length(3), // Partial prog bar
|
||||
Constraint::Length(3), // log bg
|
||||
Constraint::Length(3), // log window
|
||||
Constraint::Length(3), // status sorting
|
||||
Constraint::Length(3), // status hidden
|
||||
Constraint::Length(3), // sync browsing
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(colors_layout[3]);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_TITLE_2,
|
||||
f,
|
||||
transfer_colors_layout_col2[0],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
|
||||
f,
|
||||
transfer_colors_layout_col2[1],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
|
||||
f,
|
||||
transfer_colors_layout_col2[2],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
|
||||
f,
|
||||
transfer_colors_layout_col2[3],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_WIN,
|
||||
f,
|
||||
transfer_colors_layout_col2[4],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
|
||||
f,
|
||||
transfer_colors_layout_col2[5],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
|
||||
f,
|
||||
transfer_colors_layout_col2[6],
|
||||
);
|
||||
self.view.render(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
|
||||
f,
|
||||
transfer_colors_layout_col2[7],
|
||||
);
|
||||
// Popups
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||
if props.visible {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 40, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 50, 70);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||
}
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
||||
if props.visible {
|
||||
// make popup
|
||||
let popup = draw_area_in(f.size(), 30, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Put context back to context
|
||||
self.context = Some(ctx);
|
||||
}
|
||||
|
||||
/// ### load_styles
|
||||
///
|
||||
/// Load values from theme into input fields
|
||||
pub(crate) fn load_styles(&mut self) {
|
||||
let theme: Theme = self.theme().clone();
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_ADDR, theme.auth_address);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS, theme.auth_bookmarks);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_PASSWORD, theme.auth_password);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_PORT, theme.auth_port);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_PROTOCOL, theme.auth_protocol);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_RECENTS, theme.auth_recents);
|
||||
self.update_color(super::COMPONENT_COLOR_AUTH_USERNAME, theme.auth_username);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_ERROR, theme.misc_error_dialog);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_INPUT, theme.misc_input_dialog);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_KEYS, theme.misc_keys);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_QUIT, theme.misc_quit_dialog);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_SAVE, theme.misc_save_dialog);
|
||||
self.update_color(super::COMPONENT_COLOR_MISC_WARN, theme.misc_warn_dialog);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG,
|
||||
theme.transfer_local_explorer_background,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
|
||||
theme.transfer_local_explorer_foreground,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG,
|
||||
theme.transfer_local_explorer_highlighted,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
|
||||
theme.transfer_remote_explorer_background,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG,
|
||||
theme.transfer_remote_explorer_foreground,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
|
||||
theme.transfer_remote_explorer_highlighted,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL,
|
||||
theme.transfer_progress_bar_full,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
|
||||
theme.transfer_progress_bar_partial,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_BG,
|
||||
theme.transfer_log_background,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_LOG_WIN,
|
||||
theme.transfer_log_window,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
|
||||
theme.transfer_status_sorting,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN,
|
||||
theme.transfer_status_hidden,
|
||||
);
|
||||
self.update_color(
|
||||
super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC,
|
||||
theme.transfer_status_sync_browsing,
|
||||
);
|
||||
}
|
||||
|
||||
/// ### collect_styles
|
||||
///
|
||||
/// Collect values from input and put them into the theme.
|
||||
/// If a component has an invalid color, returns Err(component_id)
|
||||
pub(crate) fn collect_styles(&mut self) -> Result<(), &'static str> {
|
||||
// auth
|
||||
let auth_address: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_ADDR)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_ADDR)?;
|
||||
let auth_bookmarks: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_BOOKMARKS)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_BOOKMARKS)?;
|
||||
let auth_password: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_PASSWORD)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_PASSWORD)?;
|
||||
let auth_port: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_PORT)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_PORT)?;
|
||||
let auth_protocol: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_PROTOCOL)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_PROTOCOL)?;
|
||||
let auth_recents: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_RECENTS)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_RECENTS)?;
|
||||
let auth_username: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_AUTH_USERNAME)
|
||||
.map_err(|_| super::COMPONENT_COLOR_AUTH_USERNAME)?;
|
||||
// misc
|
||||
let misc_error_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_ERROR)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_ERROR)?;
|
||||
let misc_input_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_INPUT)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_INPUT)?;
|
||||
let misc_keys: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_KEYS)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_KEYS)?;
|
||||
let misc_quit_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_QUIT)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_QUIT)?;
|
||||
let misc_save_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_SAVE)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_SAVE)?;
|
||||
let misc_warn_dialog: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_MISC_WARN)
|
||||
.map_err(|_| super::COMPONENT_COLOR_MISC_WARN)?;
|
||||
// transfer
|
||||
let transfer_local_explorer_background: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG)?;
|
||||
let transfer_local_explorer_foreground: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG)?;
|
||||
let transfer_local_explorer_highlighted: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG)?;
|
||||
let transfer_remote_explorer_background: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG)?;
|
||||
let transfer_remote_explorer_foreground: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG)?;
|
||||
let transfer_remote_explorer_highlighted: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG)?;
|
||||
let transfer_log_background: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_LOG_BG)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_BG)?;
|
||||
let transfer_log_window: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_LOG_WIN)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_LOG_WIN)?;
|
||||
let transfer_progress_bar_full: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL)?;
|
||||
let transfer_progress_bar_partial: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL)?;
|
||||
let transfer_status_hidden: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN)?;
|
||||
let transfer_status_sorting: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SORTING)?;
|
||||
let transfer_status_sync_browsing: Color = self
|
||||
.get_color(super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)
|
||||
.map_err(|_| super::COMPONENT_COLOR_TRANSFER_STATUS_SYNC)?;
|
||||
// Update theme
|
||||
let mut theme: &mut Theme = self.theme_mut();
|
||||
theme.auth_address = auth_address;
|
||||
theme.auth_bookmarks = auth_bookmarks;
|
||||
theme.auth_password = auth_password;
|
||||
theme.auth_port = auth_port;
|
||||
theme.auth_protocol = auth_protocol;
|
||||
theme.auth_recents = auth_recents;
|
||||
theme.auth_username = auth_username;
|
||||
theme.misc_error_dialog = misc_error_dialog;
|
||||
theme.misc_input_dialog = misc_input_dialog;
|
||||
theme.misc_keys = misc_keys;
|
||||
theme.misc_quit_dialog = misc_quit_dialog;
|
||||
theme.misc_save_dialog = misc_save_dialog;
|
||||
theme.misc_warn_dialog = misc_warn_dialog;
|
||||
theme.transfer_local_explorer_background = transfer_local_explorer_background;
|
||||
theme.transfer_local_explorer_foreground = transfer_local_explorer_foreground;
|
||||
theme.transfer_local_explorer_highlighted = transfer_local_explorer_highlighted;
|
||||
theme.transfer_remote_explorer_background = transfer_remote_explorer_background;
|
||||
theme.transfer_remote_explorer_foreground = transfer_remote_explorer_foreground;
|
||||
theme.transfer_remote_explorer_highlighted = transfer_remote_explorer_highlighted;
|
||||
theme.transfer_log_background = transfer_log_background;
|
||||
theme.transfer_log_window = transfer_log_window;
|
||||
theme.transfer_progress_bar_full = transfer_progress_bar_full;
|
||||
theme.transfer_progress_bar_partial = transfer_progress_bar_partial;
|
||||
theme.transfer_status_hidden = transfer_status_hidden;
|
||||
theme.transfer_status_sorting = transfer_status_sorting;
|
||||
theme.transfer_status_sync_browsing = transfer_status_sync_browsing;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### update_color
|
||||
///
|
||||
/// Update color for provided component
|
||||
fn update_color(&mut self, component: &str, color: Color) {
|
||||
if let Some(props) = self.view.get_props(component) {
|
||||
self.view.update(
|
||||
component,
|
||||
ColorPickerPropsBuilder::from(props)
|
||||
.with_color(&color)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_color
|
||||
///
|
||||
/// Get color from component
|
||||
fn get_color(&self, component: &str) -> Result<Color, ()> {
|
||||
match self.view.get_state(component) {
|
||||
Some(Payload::One(Value::Str(color))) => match parse_color(color.as_str()) {
|
||||
Some(c) => Ok(c),
|
||||
None => Err(()),
|
||||
},
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### mount_color_picker
|
||||
///
|
||||
/// Mount color picker with provided data
|
||||
fn mount_color_picker(&mut self, id: &str, label: &str) {
|
||||
self.view.mount(
|
||||
id,
|
||||
Box::new(ColorPicker::new(
|
||||
ColorPickerPropsBuilder::default()
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::Reset)
|
||||
.with_label(label.to_string())
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
/// ### mount_title
|
||||
///
|
||||
/// Mount title
|
||||
fn mount_title(&mut self, id: &str, text: &str) {
|
||||
self.view.mount(
|
||||
id,
|
||||
Box::new(Label::new(
|
||||
LabelPropsBuilder::default()
|
||||
.bold()
|
||||
.with_text(text.to_string())
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
300
src/ui/components/color_picker.rs
Normal file
300
src/ui/components/color_picker.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
//! ## ColorPicker
|
||||
//!
|
||||
//! `ColorPicker` component extends an `Input` component in order to provide some extra features
|
||||
//! for the color picker.
|
||||
|
||||
/**
|
||||
* 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::fmt_color;
|
||||
use crate::utils::parser::parse_color;
|
||||
// ext
|
||||
use tuirealm::components::input::{Input, InputPropsBuilder};
|
||||
use tuirealm::event::Event;
|
||||
use tuirealm::props::{Props, PropsBuilder};
|
||||
use tuirealm::tui::{
|
||||
layout::Rect,
|
||||
style::Color,
|
||||
widgets::{BorderType, Borders},
|
||||
};
|
||||
use tuirealm::{Canvas, Component, Msg, Payload, Value};
|
||||
|
||||
// -- props
|
||||
|
||||
/// ## ColorPickerPropsBuilder
|
||||
///
|
||||
/// A wrapper around an `InputPropsBuilder`
|
||||
pub struct ColorPickerPropsBuilder {
|
||||
puppet: InputPropsBuilder,
|
||||
}
|
||||
|
||||
impl Default for ColorPickerPropsBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
puppet: InputPropsBuilder::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsBuilder for ColorPickerPropsBuilder {
|
||||
fn build(&mut self) -> Props {
|
||||
self.puppet.build()
|
||||
}
|
||||
|
||||
fn hidden(&mut self) -> &mut Self {
|
||||
self.puppet.hidden();
|
||||
self
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut Self {
|
||||
self.puppet.visible();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Props> for ColorPickerPropsBuilder {
|
||||
fn from(props: Props) -> Self {
|
||||
ColorPickerPropsBuilder {
|
||||
puppet: InputPropsBuilder::from(props),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorPickerPropsBuilder {
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
pub fn with_borders(
|
||||
&mut self,
|
||||
borders: Borders,
|
||||
variant: BorderType,
|
||||
color: Color,
|
||||
) -> &mut Self {
|
||||
self.puppet.with_borders(borders, variant, color);
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_label
|
||||
///
|
||||
/// Set input label
|
||||
pub fn with_label(&mut self, label: String) -> &mut Self {
|
||||
self.puppet.with_label(label);
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_color
|
||||
///
|
||||
/// Set initial value for component
|
||||
pub fn with_color(&mut self, color: &Color) -> &mut Self {
|
||||
self.puppet.with_value(fmt_color(color));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// -- component
|
||||
|
||||
/// ## ColorPicker
|
||||
///
|
||||
/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker
|
||||
pub struct ColorPicker {
|
||||
input: Input,
|
||||
}
|
||||
|
||||
impl ColorPicker {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new `ColorPicker`
|
||||
pub fn new(props: Props) -> Self {
|
||||
// Instantiate a new color picker using input
|
||||
Self {
|
||||
input: Input::new(props),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### update_colors
|
||||
///
|
||||
/// Update colors to match selected color, with provided one
|
||||
fn update_colors(&mut self, color: Color) {
|
||||
let mut props = self.get_props();
|
||||
props.foreground = color;
|
||||
props.borders.color = color;
|
||||
let _ = self.input.update(props);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ColorPicker {
|
||||
/// ### render
|
||||
///
|
||||
/// 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) {
|
||||
self.input.render(render, area);
|
||||
}
|
||||
|
||||
/// ### update
|
||||
///
|
||||
/// Update component properties
|
||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
||||
/// existing properties and then edited before calling update.
|
||||
/// Returns a Msg to the view
|
||||
fn update(&mut self, props: Props) -> Msg {
|
||||
let msg: Msg = self.input.update(props);
|
||||
match msg {
|
||||
Msg::OnChange(Payload::One(Value::Str(input))) => match parse_color(input.as_str()) {
|
||||
Some(color) => {
|
||||
// Update color and return OK
|
||||
self.update_colors(color);
|
||||
Msg::OnChange(Payload::One(Value::Str(input)))
|
||||
}
|
||||
None => {
|
||||
// Invalid color
|
||||
self.update_colors(Color::Red);
|
||||
Msg::None
|
||||
}
|
||||
},
|
||||
msg => msg,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_props
|
||||
///
|
||||
/// Returns a props builder starting from component properties.
|
||||
/// This returns a prop builder in order to make easier to create
|
||||
/// new properties for the element.
|
||||
fn get_props(&self) -> Props {
|
||||
self.input.get_props()
|
||||
}
|
||||
|
||||
/// ### on
|
||||
///
|
||||
/// Handle input event and update internal states.
|
||||
/// Returns a Msg to the view
|
||||
fn on(&mut self, ev: Event) -> Msg {
|
||||
// Capture message from input
|
||||
match self.input.on(ev) {
|
||||
Msg::OnChange(Payload::One(Value::Str(input))) => {
|
||||
// Capture color and validate
|
||||
match parse_color(input.as_str()) {
|
||||
Some(color) => {
|
||||
// Update color and return OK
|
||||
self.update_colors(color);
|
||||
Msg::OnChange(Payload::One(Value::Str(input)))
|
||||
}
|
||||
None => {
|
||||
// Invalid color
|
||||
self.update_colors(Color::Red);
|
||||
Msg::None
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::OnSubmit(_) => Msg::None,
|
||||
msg => msg,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_state
|
||||
///
|
||||
/// Get current state from component
|
||||
/// For this component returns Unsigned if the input type is a number, otherwise a text
|
||||
/// The value is always the current input.
|
||||
fn get_state(&self) -> Payload {
|
||||
match self.input.get_state() {
|
||||
Payload::One(Value::Str(color)) => match parse_color(color.as_str()) {
|
||||
None => Payload::None,
|
||||
Some(_) => Payload::One(Value::Str(color)),
|
||||
},
|
||||
_ => Payload::None,
|
||||
}
|
||||
}
|
||||
|
||||
// -- events
|
||||
|
||||
/// ### blur
|
||||
///
|
||||
/// Blur component; basically remove focus
|
||||
fn blur(&mut self) {
|
||||
self.input.blur();
|
||||
}
|
||||
|
||||
/// ### active
|
||||
///
|
||||
/// Active component; basically give focus
|
||||
fn active(&mut self) {
|
||||
self.input.active();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_ui_components_color_picker() {
|
||||
let mut component: ColorPicker = ColorPicker::new(
|
||||
ColorPickerPropsBuilder::default()
|
||||
.visible()
|
||||
.with_color(&Color::Rgb(204, 170, 0))
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0))
|
||||
.build(),
|
||||
);
|
||||
// Focus
|
||||
component.blur();
|
||||
component.active();
|
||||
// Get value
|
||||
assert_eq!(
|
||||
component.get_state(),
|
||||
Payload::One(Value::Str(String::from("#ccaa00")))
|
||||
);
|
||||
// Set an invalid color
|
||||
let props = InputPropsBuilder::from(component.get_props())
|
||||
.with_value(String::from("#pippo1"))
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(component.update(props), Msg::None);
|
||||
assert_eq!(component.get_state(), Payload::None);
|
||||
// Reset color
|
||||
let props = ColorPickerPropsBuilder::from(component.get_props())
|
||||
.with_color(&Color::Rgb(204, 170, 0))
|
||||
.hidden()
|
||||
.build();
|
||||
assert_eq!(
|
||||
component.update(props),
|
||||
Msg::OnChange(Payload::One(Value::Str("#ccaa00".to_string())))
|
||||
);
|
||||
// Backspace (invalid)
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||
Msg::None
|
||||
);
|
||||
// Press '1'
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Char('1')))),
|
||||
Msg::OnChange(Payload::One(Value::Str(String::from("#ccaa01"))))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,9 @@
|
||||
// ext
|
||||
use tuirealm::components::utils::get_block;
|
||||
use tuirealm::event::{Event, KeyCode, KeyModifiers};
|
||||
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
|
||||
use tuirealm::props::{
|
||||
BordersProps, PropPayload, PropValue, Props, PropsBuilder, TextParts, TextSpan,
|
||||
};
|
||||
use tuirealm::tui::{
|
||||
layout::{Corner, Rect},
|
||||
style::{Color, Style},
|
||||
@@ -39,6 +41,8 @@ use tuirealm::{Canvas, Component, Msg, Payload, Value};
|
||||
|
||||
// -- props
|
||||
|
||||
const PROP_HIGHLIGHT_COLOR: &str = "props-highlight-color";
|
||||
|
||||
pub struct FileListPropsBuilder {
|
||||
props: Option<Props>,
|
||||
}
|
||||
@@ -98,6 +102,19 @@ impl FileListPropsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_highlight_color
|
||||
///
|
||||
/// 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)),
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_borders
|
||||
///
|
||||
/// Set component borders style
|
||||
@@ -306,9 +323,13 @@ impl Component for FileList {
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let (fg, bg): (Color, Color) = match self.states.focus {
|
||||
true => (Color::Black, self.props.background),
|
||||
false => (self.props.foreground, Color::Reset),
|
||||
let highlighted_color: Color = match self.props.own.get(PROP_HIGHLIGHT_COLOR) {
|
||||
Some(PropPayload::One(PropValue::Color(c))) => *c,
|
||||
_ => Color::Reset,
|
||||
};
|
||||
let (h_fg, h_bg): (Color, Color) = match self.states.focus {
|
||||
true => (Color::Black, highlighted_color),
|
||||
false => (highlighted_color, self.props.background),
|
||||
};
|
||||
// Render
|
||||
let mut state: ListState = ListState::default();
|
||||
@@ -321,10 +342,15 @@ impl Component for FileList {
|
||||
self.states.focus,
|
||||
))
|
||||
.start_corner(Corner::TopLeft)
|
||||
.style(
|
||||
Style::default()
|
||||
.fg(self.props.foreground)
|
||||
.bg(self.props.background),
|
||||
)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(bg)
|
||||
.fg(fg)
|
||||
.bg(h_bg)
|
||||
.fg(h_fg)
|
||||
.add_modifier(self.props.modifiers),
|
||||
),
|
||||
area,
|
||||
@@ -393,10 +419,7 @@ impl Component for FileList {
|
||||
self.states.toggle_file(self.states.list_index());
|
||||
Msg::None
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Report event
|
||||
Msg::OnSubmit(self.get_state())
|
||||
}
|
||||
KeyCode::Enter => Msg::OnSubmit(self.get_state()),
|
||||
_ => {
|
||||
// Return key event to activity
|
||||
Msg::OnKey(key)
|
||||
@@ -449,7 +472,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tuirealm::event::KeyEvent;
|
||||
use tuirealm::event::{KeyEvent, KeyModifiers};
|
||||
|
||||
#[test]
|
||||
fn test_ui_components_file_list_states() {
|
||||
@@ -526,6 +549,7 @@ mod tests {
|
||||
.visible()
|
||||
.with_foreground(Color::Red)
|
||||
.with_background(Color::Blue)
|
||||
.with_highlight_color(Color::LightRed)
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||
.with_files(
|
||||
Some(String::from("files")),
|
||||
@@ -533,6 +557,10 @@ mod tests {
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
assert_eq!(
|
||||
*component.props.own.get(PROP_HIGHLIGHT_COLOR).unwrap(),
|
||||
PropPayload::One(PropValue::Color(Color::LightRed))
|
||||
);
|
||||
assert_eq!(component.props.foreground, Color::Red);
|
||||
assert_eq!(component.props.background, Color::Blue);
|
||||
assert_eq!(component.props.visible, true);
|
||||
@@ -626,6 +654,15 @@ mod tests {
|
||||
component.on(Event::Key(KeyEvent::from(KeyCode::Char('a')))),
|
||||
Msg::OnKey(KeyEvent::from(KeyCode::Char('a')))
|
||||
);
|
||||
// Ctrl + a
|
||||
assert_eq!(
|
||||
component.on(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL
|
||||
))),
|
||||
Msg::None
|
||||
);
|
||||
assert_eq!(component.states.selected.len(), component.states.list_len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -96,6 +96,16 @@ impl LogboxPropsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_background
|
||||
///
|
||||
/// Set background color for area
|
||||
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.background = color;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_log(&mut self, title: Option<String>, table: TextTable) -> &mut Self {
|
||||
if let Some(props) = self.props.as_mut() {
|
||||
props.texts = TextParts::table(title, table);
|
||||
@@ -219,6 +229,7 @@ impl Component for LogBox {
|
||||
))
|
||||
.start_corner(Corner::BottomLeft)
|
||||
.highlight_symbol(">> ")
|
||||
.style(Style::default().bg(self.props.background))
|
||||
.highlight_style(Style::default().add_modifier(self.props.modifiers));
|
||||
let mut state: ListState = ListState::default();
|
||||
state.select(Some(self.states.list_index));
|
||||
@@ -311,6 +322,7 @@ mod tests {
|
||||
.hidden()
|
||||
.visible()
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||
.with_background(Color::Blue)
|
||||
.with_log(
|
||||
Some(String::from("Log")),
|
||||
TableBuilder::default()
|
||||
@@ -324,6 +336,7 @@ mod tests {
|
||||
.build(),
|
||||
);
|
||||
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"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
*/
|
||||
// exports
|
||||
pub mod bookmark_list;
|
||||
pub mod color_picker;
|
||||
pub mod file_list;
|
||||
pub mod logbox;
|
||||
pub mod msgbox;
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// deps
|
||||
extern crate textwrap;
|
||||
extern crate tuirealm;
|
||||
// locals
|
||||
use crate::utils::fmt::align_text_center;
|
||||
// ext
|
||||
@@ -97,6 +94,13 @@ impl MsgBoxPropsBuilder {
|
||||
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,
|
||||
@@ -224,6 +228,7 @@ mod tests {
|
||||
.visible()
|
||||
.with_foreground(Color::Red)
|
||||
.bold()
|
||||
.blink()
|
||||
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||
.with_texts(
|
||||
None,
|
||||
|
||||
@@ -25,54 +25,45 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Dependencies
|
||||
extern crate crossterm;
|
||||
extern crate tuirealm;
|
||||
|
||||
// Locals
|
||||
use super::input::InputHandler;
|
||||
use super::store::Store;
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::theme_provider::ThemeProvider;
|
||||
|
||||
// Includes
|
||||
use crossterm::event::DisableMouseCapture;
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use std::io::{stdout, Stdout};
|
||||
use std::path::PathBuf;
|
||||
use tuirealm::tui::backend::CrosstermBackend;
|
||||
use tuirealm::tui::Terminal;
|
||||
|
||||
type TuiTerminal = Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
/// ## Context
|
||||
///
|
||||
/// Context holds data structures used by the ui
|
||||
pub struct Context {
|
||||
pub ft_params: Option<FileTransferParams>,
|
||||
pub(crate) config_client: Option<ConfigClient>,
|
||||
ft_params: Option<FileTransferParams>,
|
||||
config_client: ConfigClient,
|
||||
pub(crate) store: Store,
|
||||
pub(crate) input_hnd: InputHandler,
|
||||
pub(crate) terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
input_hnd: InputHandler,
|
||||
pub(crate) terminal: TuiTerminal,
|
||||
theme_provider: ThemeProvider,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
/// ### FileTransferParams
|
||||
///
|
||||
/// Holds connection parameters for file transfers
|
||||
pub struct FileTransferParams {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub entry_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new Context
|
||||
pub fn new(config_client: Option<ConfigClient>, error: Option<String>) -> Context {
|
||||
pub fn new(
|
||||
config_client: ConfigClient,
|
||||
theme_provider: ThemeProvider,
|
||||
error: Option<String>,
|
||||
) -> Context {
|
||||
// Create terminal
|
||||
let mut stdout = stdout();
|
||||
assert!(execute!(stdout, EnterAlternateScreen).is_ok());
|
||||
@@ -82,10 +73,57 @@ impl Context {
|
||||
store: Store::init(),
|
||||
input_hnd: InputHandler::new(),
|
||||
terminal: Terminal::new(CrosstermBackend::new(stdout)).unwrap(),
|
||||
theme_provider,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
// -- getters
|
||||
|
||||
pub fn ft_params(&self) -> Option<&FileTransferParams> {
|
||||
self.ft_params.as_ref()
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &ConfigClient {
|
||||
&self.config_client
|
||||
}
|
||||
|
||||
pub fn config_mut(&mut self) -> &mut ConfigClient {
|
||||
&mut self.config_client
|
||||
}
|
||||
|
||||
pub(crate) fn input_hnd(&self) -> &InputHandler {
|
||||
&self.input_hnd
|
||||
}
|
||||
|
||||
pub(crate) fn store(&self) -> &Store {
|
||||
&self.store
|
||||
}
|
||||
|
||||
pub(crate) fn store_mut(&mut self) -> &mut Store {
|
||||
&mut self.store
|
||||
}
|
||||
|
||||
pub fn theme_provider(&self) -> &ThemeProvider {
|
||||
&self.theme_provider
|
||||
}
|
||||
|
||||
pub fn theme_provider_mut(&mut self) -> &mut ThemeProvider {
|
||||
&mut self.theme_provider
|
||||
}
|
||||
|
||||
pub fn terminal(&mut self) -> &mut TuiTerminal {
|
||||
&mut self.terminal
|
||||
}
|
||||
|
||||
// -- setter
|
||||
|
||||
pub fn set_ftparams(&mut self, params: FileTransferParams) {
|
||||
self.ft_params = Some(params);
|
||||
}
|
||||
|
||||
// -- error
|
||||
|
||||
/// ### set_error
|
||||
///
|
||||
/// Set context error
|
||||
@@ -93,10 +131,10 @@ impl Context {
|
||||
self.error = Some(err);
|
||||
}
|
||||
|
||||
/// ### get_error
|
||||
/// ### error
|
||||
///
|
||||
/// Get error message and remove it from the context
|
||||
pub fn get_error(&mut self) -> Option<String> {
|
||||
pub fn error(&mut self) -> Option<String> {
|
||||
self.error.take()
|
||||
}
|
||||
|
||||
@@ -146,57 +184,3 @@ impl Drop for Context {
|
||||
self.leave_alternate_screen();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileTransferParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
address: String::new(),
|
||||
port: 22,
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: None,
|
||||
password: None,
|
||||
entry_directory: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_ui_context_ft_params() {
|
||||
let params: FileTransferParams = FileTransferParams::default();
|
||||
assert_eq!(params.address.as_str(), "");
|
||||
assert_eq!(params.port, 22);
|
||||
assert_eq!(params.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(params.username.is_none());
|
||||
assert!(params.password.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "github-actions"))]
|
||||
fn test_ui_context() {
|
||||
// Prepare stuff
|
||||
let mut ctx: Context = Context::new(None, Some(String::from("alles kaput")));
|
||||
assert!(ctx.error.is_some());
|
||||
assert_eq!(ctx.get_error().unwrap().as_str(), "alles kaput");
|
||||
assert!(ctx.error.is_none());
|
||||
assert!(ctx.get_error().is_none());
|
||||
ctx.set_error(String::from("err"));
|
||||
assert!(ctx.error.is_some());
|
||||
assert!(ctx.get_error().is_some());
|
||||
assert!(ctx.get_error().is_none());
|
||||
// Try other methods
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
ctx.enter_alternate_screen();
|
||||
ctx.clear_screen();
|
||||
ctx.leave_alternate_screen();
|
||||
}
|
||||
drop(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
extern crate crossterm;
|
||||
|
||||
use crossterm::event::{poll, read, Event};
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ pub const MSG_KEY_CHAR_L: Msg = Msg::OnKey(KeyEvent {
|
||||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
/*
|
||||
pub const MSG_KEY_CHAR_M: Msg = Msg::OnKey(KeyEvent {
|
||||
pub const MSG_KEY_CHAR_M: Msg = Msg::OnKey(KeyEvent { NOTE: used for mark
|
||||
code: KeyCode::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
@@ -165,7 +165,6 @@ pub const MSG_KEY_CHAR_U: Msg = Msg::OnKey(KeyEvent {
|
||||
code: KeyCode::Char('u'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
/*
|
||||
pub const MSG_KEY_CHAR_V: Msg = Msg::OnKey(KeyEvent {
|
||||
code: KeyCode::Char('v'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
@@ -174,7 +173,6 @@ pub const MSG_KEY_CHAR_W: Msg = Msg::OnKey(KeyEvent {
|
||||
code: KeyCode::Char('w'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
*/
|
||||
pub const MSG_KEY_CHAR_X: Msg = Msg::OnKey(KeyEvent {
|
||||
code: KeyCode::Char('x'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
extern crate magic_crypt;
|
||||
|
||||
// Ext
|
||||
use magic_crypt::MagicCryptTrait;
|
||||
|
||||
|
||||
448
src/utils/fmt.rs
448
src/utils/fmt.rs
@@ -25,12 +25,10 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
extern crate chrono;
|
||||
extern crate textwrap;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tuirealm::tui::style::Color;
|
||||
|
||||
/// ### fmt_pex
|
||||
///
|
||||
@@ -122,10 +120,19 @@ pub fn align_text_center(text: &str, width: u16) -> String {
|
||||
/// ### elide_path
|
||||
///
|
||||
/// Elide a path if longer than width
|
||||
/// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME}
|
||||
/// In this case, the path is formatted to {ANCESTOR[0]}/…/{PARENT[0]}/{BASENAME}
|
||||
pub fn fmt_path_elide(p: &Path, width: usize) -> String {
|
||||
fmt_path_elide_ex(p, width, 0)
|
||||
}
|
||||
|
||||
/// ### fmt_path_elide_ex
|
||||
///
|
||||
/// Elide a path if longer than width
|
||||
/// In this case, the path is formatted to {ANCESTOR[0]}/…/{PARENT[0]}/{BASENAME}
|
||||
/// This function allows to specify an extra length to consider to elide path
|
||||
pub fn fmt_path_elide_ex(p: &Path, width: usize, extra_len: usize) -> String {
|
||||
let fmt_path: String = format!("{}", p.display());
|
||||
match fmt_path.len() > width as usize {
|
||||
match fmt_path.len() + extra_len > width as usize {
|
||||
false => fmt_path,
|
||||
true => {
|
||||
// Elide
|
||||
@@ -136,9 +143,9 @@ pub fn fmt_path_elide(p: &Path, width: usize) -> String {
|
||||
if ancestors_len > 2 {
|
||||
elided_path.push(ancestors.nth(ancestors_len - 2).unwrap());
|
||||
}
|
||||
// If ancestors_len is bigger than 3, push '...' and parent too
|
||||
// If ancestors_len is bigger than 3, push '…' and parent too
|
||||
if ancestors_len > 3 {
|
||||
elided_path.push("...");
|
||||
elided_path.push("…");
|
||||
if let Some(parent) = p.ancestors().nth(1) {
|
||||
elided_path.push(parent.file_name().unwrap());
|
||||
}
|
||||
@@ -152,6 +159,174 @@ pub fn fmt_path_elide(p: &Path, width: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt_color
|
||||
///
|
||||
/// Format color
|
||||
pub fn fmt_color(color: &Color) -> String {
|
||||
match color {
|
||||
Color::Black => "Black".to_string(),
|
||||
Color::Blue => "Blue".to_string(),
|
||||
Color::Cyan => "Cyan".to_string(),
|
||||
Color::DarkGray => "DarkGray".to_string(),
|
||||
Color::Gray => "Gray".to_string(),
|
||||
Color::Green => "Green".to_string(),
|
||||
Color::LightBlue => "LightBlue".to_string(),
|
||||
Color::LightCyan => "LightCyan".to_string(),
|
||||
Color::LightGreen => "LightGreen".to_string(),
|
||||
Color::LightMagenta => "LightMagenta".to_string(),
|
||||
Color::LightRed => "LightRed".to_string(),
|
||||
Color::LightYellow => "LightYellow".to_string(),
|
||||
Color::Magenta => "Magenta".to_string(),
|
||||
Color::Red => "Red".to_string(),
|
||||
Color::Reset => "Default".to_string(),
|
||||
Color::White => "White".to_string(),
|
||||
Color::Yellow => "Yellow".to_string(),
|
||||
Color::Indexed(_) => "Default".to_string(),
|
||||
// -- css colors
|
||||
Color::Rgb(240, 248, 255) => "aliceblue".to_string(),
|
||||
Color::Rgb(250, 235, 215) => "antiquewhite".to_string(),
|
||||
Color::Rgb(0, 255, 255) => "aqua".to_string(),
|
||||
Color::Rgb(127, 255, 212) => "aquamarine".to_string(),
|
||||
Color::Rgb(240, 255, 255) => "azure".to_string(),
|
||||
Color::Rgb(245, 245, 220) => "beige".to_string(),
|
||||
Color::Rgb(255, 228, 196) => "bisque".to_string(),
|
||||
Color::Rgb(0, 0, 0) => "black".to_string(),
|
||||
Color::Rgb(255, 235, 205) => "blanchedalmond".to_string(),
|
||||
Color::Rgb(0, 0, 255) => "blue".to_string(),
|
||||
Color::Rgb(138, 43, 226) => "blueviolet".to_string(),
|
||||
Color::Rgb(165, 42, 42) => "brown".to_string(),
|
||||
Color::Rgb(222, 184, 135) => "burlywood".to_string(),
|
||||
Color::Rgb(95, 158, 160) => "cadetblue".to_string(),
|
||||
Color::Rgb(127, 255, 0) => "chartreuse".to_string(),
|
||||
Color::Rgb(210, 105, 30) => "chocolate".to_string(),
|
||||
Color::Rgb(255, 127, 80) => "coral".to_string(),
|
||||
Color::Rgb(100, 149, 237) => "cornflowerblue".to_string(),
|
||||
Color::Rgb(255, 248, 220) => "cornsilk".to_string(),
|
||||
Color::Rgb(220, 20, 60) => "crimson".to_string(),
|
||||
Color::Rgb(0, 0, 139) => "darkblue".to_string(),
|
||||
Color::Rgb(0, 139, 139) => "darkcyan".to_string(),
|
||||
Color::Rgb(184, 134, 11) => "darkgoldenrod".to_string(),
|
||||
Color::Rgb(169, 169, 169) => "darkgray".to_string(),
|
||||
Color::Rgb(0, 100, 0) => "darkgreen".to_string(),
|
||||
Color::Rgb(189, 183, 107) => "darkkhaki".to_string(),
|
||||
Color::Rgb(139, 0, 139) => "darkmagenta".to_string(),
|
||||
Color::Rgb(85, 107, 47) => "darkolivegreen".to_string(),
|
||||
Color::Rgb(255, 140, 0) => "darkorange".to_string(),
|
||||
Color::Rgb(153, 50, 204) => "darkorchid".to_string(),
|
||||
Color::Rgb(139, 0, 0) => "darkred".to_string(),
|
||||
Color::Rgb(233, 150, 122) => "darksalmon".to_string(),
|
||||
Color::Rgb(143, 188, 143) => "darkseagreen".to_string(),
|
||||
Color::Rgb(72, 61, 139) => "darkslateblue".to_string(),
|
||||
Color::Rgb(47, 79, 79) => "darkslategray".to_string(),
|
||||
Color::Rgb(0, 206, 209) => "darkturquoise".to_string(),
|
||||
Color::Rgb(148, 0, 211) => "darkviolet".to_string(),
|
||||
Color::Rgb(255, 20, 147) => "deeppink".to_string(),
|
||||
Color::Rgb(0, 191, 255) => "deepskyblue".to_string(),
|
||||
Color::Rgb(105, 105, 105) => "dimgray".to_string(),
|
||||
Color::Rgb(30, 144, 255) => "dodgerblue".to_string(),
|
||||
Color::Rgb(178, 34, 34) => "firebrick".to_string(),
|
||||
Color::Rgb(255, 250, 240) => "floralwhite".to_string(),
|
||||
Color::Rgb(34, 139, 34) => "forestgreen".to_string(),
|
||||
Color::Rgb(255, 0, 255) => "fuchsia".to_string(),
|
||||
Color::Rgb(220, 220, 220) => "gainsboro".to_string(),
|
||||
Color::Rgb(248, 248, 255) => "ghostwhite".to_string(),
|
||||
Color::Rgb(255, 215, 0) => "gold".to_string(),
|
||||
Color::Rgb(218, 165, 32) => "goldenrod".to_string(),
|
||||
Color::Rgb(128, 128, 128) => "gray".to_string(),
|
||||
Color::Rgb(0, 128, 0) => "green".to_string(),
|
||||
Color::Rgb(173, 255, 47) => "greenyellow".to_string(),
|
||||
Color::Rgb(240, 255, 240) => "honeydew".to_string(),
|
||||
Color::Rgb(255, 105, 180) => "hotpink".to_string(),
|
||||
Color::Rgb(205, 92, 92) => "indianred".to_string(),
|
||||
Color::Rgb(75, 0, 130) => "indigo".to_string(),
|
||||
Color::Rgb(255, 255, 240) => "ivory".to_string(),
|
||||
Color::Rgb(240, 230, 140) => "khaki".to_string(),
|
||||
Color::Rgb(230, 230, 250) => "lavender".to_string(),
|
||||
Color::Rgb(255, 240, 245) => "lavenderblush".to_string(),
|
||||
Color::Rgb(124, 252, 0) => "lawngreen".to_string(),
|
||||
Color::Rgb(255, 250, 205) => "lemonchiffon".to_string(),
|
||||
Color::Rgb(173, 216, 230) => "lightblue".to_string(),
|
||||
Color::Rgb(240, 128, 128) => "lightcoral".to_string(),
|
||||
Color::Rgb(224, 255, 255) => "lightcyan".to_string(),
|
||||
Color::Rgb(250, 250, 210) => "lightgoldenrodyellow".to_string(),
|
||||
Color::Rgb(211, 211, 211) => "lightgray".to_string(),
|
||||
Color::Rgb(144, 238, 144) => "lightgreen".to_string(),
|
||||
Color::Rgb(255, 182, 193) => "lightpink".to_string(),
|
||||
Color::Rgb(255, 160, 122) => "lightsalmon".to_string(),
|
||||
Color::Rgb(32, 178, 170) => "lightseagreen".to_string(),
|
||||
Color::Rgb(135, 206, 250) => "lightskyblue".to_string(),
|
||||
Color::Rgb(119, 136, 153) => "lightslategray".to_string(),
|
||||
Color::Rgb(176, 196, 222) => "lightsteelblue".to_string(),
|
||||
Color::Rgb(255, 255, 224) => "lightyellow".to_string(),
|
||||
Color::Rgb(0, 255, 0) => "lime".to_string(),
|
||||
Color::Rgb(50, 205, 50) => "limegreen".to_string(),
|
||||
Color::Rgb(250, 240, 230) => "linen".to_string(),
|
||||
Color::Rgb(128, 0, 0) => "maroon".to_string(),
|
||||
Color::Rgb(102, 205, 170) => "mediumaquamarine".to_string(),
|
||||
Color::Rgb(0, 0, 205) => "mediumblue".to_string(),
|
||||
Color::Rgb(186, 85, 211) => "mediumorchid".to_string(),
|
||||
Color::Rgb(147, 112, 219) => "mediumpurple".to_string(),
|
||||
Color::Rgb(60, 179, 113) => "mediumseagreen".to_string(),
|
||||
Color::Rgb(123, 104, 238) => "mediumslateblue".to_string(),
|
||||
Color::Rgb(0, 250, 154) => "mediumspringgreen".to_string(),
|
||||
Color::Rgb(72, 209, 204) => "mediumturquoise".to_string(),
|
||||
Color::Rgb(199, 21, 133) => "mediumvioletred".to_string(),
|
||||
Color::Rgb(25, 25, 112) => "midnightblue".to_string(),
|
||||
Color::Rgb(245, 255, 250) => "mintcream".to_string(),
|
||||
Color::Rgb(255, 228, 225) => "mistyrose".to_string(),
|
||||
Color::Rgb(255, 228, 181) => "moccasin".to_string(),
|
||||
Color::Rgb(255, 222, 173) => "navajowhite".to_string(),
|
||||
Color::Rgb(0, 0, 128) => "navy".to_string(),
|
||||
Color::Rgb(253, 245, 230) => "oldlace".to_string(),
|
||||
Color::Rgb(128, 128, 0) => "olive".to_string(),
|
||||
Color::Rgb(107, 142, 35) => "olivedrab".to_string(),
|
||||
Color::Rgb(255, 165, 0) => "orange".to_string(),
|
||||
Color::Rgb(255, 69, 0) => "orangered".to_string(),
|
||||
Color::Rgb(218, 112, 214) => "orchid".to_string(),
|
||||
Color::Rgb(238, 232, 170) => "palegoldenrod".to_string(),
|
||||
Color::Rgb(152, 251, 152) => "palegreen".to_string(),
|
||||
Color::Rgb(175, 238, 238) => "paleturquoise".to_string(),
|
||||
Color::Rgb(219, 112, 147) => "palevioletred".to_string(),
|
||||
Color::Rgb(255, 239, 213) => "papayawhip".to_string(),
|
||||
Color::Rgb(255, 218, 185) => "peachpuff".to_string(),
|
||||
Color::Rgb(205, 133, 63) => "peru".to_string(),
|
||||
Color::Rgb(255, 192, 203) => "pink".to_string(),
|
||||
Color::Rgb(221, 160, 221) => "plum".to_string(),
|
||||
Color::Rgb(176, 224, 230) => "powderblue".to_string(),
|
||||
Color::Rgb(128, 0, 128) => "purple".to_string(),
|
||||
Color::Rgb(102, 51, 153) => "rebeccapurple".to_string(),
|
||||
Color::Rgb(255, 0, 0) => "red".to_string(),
|
||||
Color::Rgb(188, 143, 143) => "rosybrown".to_string(),
|
||||
Color::Rgb(65, 105, 225) => "royalblue".to_string(),
|
||||
Color::Rgb(139, 69, 19) => "saddlebrown".to_string(),
|
||||
Color::Rgb(250, 128, 114) => "salmon".to_string(),
|
||||
Color::Rgb(244, 164, 96) => "sandybrown".to_string(),
|
||||
Color::Rgb(46, 139, 87) => "seagreen".to_string(),
|
||||
Color::Rgb(255, 245, 238) => "seashell".to_string(),
|
||||
Color::Rgb(160, 82, 45) => "sienna".to_string(),
|
||||
Color::Rgb(192, 192, 192) => "silver".to_string(),
|
||||
Color::Rgb(135, 206, 235) => "skyblue".to_string(),
|
||||
Color::Rgb(106, 90, 205) => "slateblue".to_string(),
|
||||
Color::Rgb(112, 128, 144) => "slategray".to_string(),
|
||||
Color::Rgb(255, 250, 250) => "snow".to_string(),
|
||||
Color::Rgb(0, 255, 127) => "springgreen".to_string(),
|
||||
Color::Rgb(70, 130, 180) => "steelblue".to_string(),
|
||||
Color::Rgb(210, 180, 140) => "tan".to_string(),
|
||||
Color::Rgb(0, 128, 128) => "teal".to_string(),
|
||||
Color::Rgb(216, 191, 216) => "thistle".to_string(),
|
||||
Color::Rgb(255, 99, 71) => "tomato".to_string(),
|
||||
Color::Rgb(64, 224, 208) => "turquoise".to_string(),
|
||||
Color::Rgb(238, 130, 238) => "violet".to_string(),
|
||||
Color::Rgb(245, 222, 179) => "wheat".to_string(),
|
||||
Color::Rgb(255, 255, 255) => "white".to_string(),
|
||||
Color::Rgb(245, 245, 245) => "whitesmoke".to_string(),
|
||||
Color::Rgb(255, 255, 0) => "yellow".to_string(),
|
||||
Color::Rgb(154, 205, 50) => "yellowgreen".to_string(),
|
||||
// -- others
|
||||
Color::Rgb(r, g, b) => format!("#{:02x}{:02x}{:02x}", r, g, b),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### shadow_password
|
||||
///
|
||||
/// Return a string with the same length of input string, but each character is replaced by '*'
|
||||
@@ -224,7 +399,264 @@ mod tests {
|
||||
// Above max size, only one ancestor
|
||||
assert_eq!(fmt_path_elide(p, 8), String::from("/develop/pippo"));
|
||||
let p: &Path = &Path::new("/develop/pippo/foo/bar");
|
||||
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/.../foo/bar"));
|
||||
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/…/foo/bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_fmt_color() {
|
||||
assert_eq!(fmt_color(&Color::Black).as_str(), "Black");
|
||||
assert_eq!(fmt_color(&Color::Blue).as_str(), "Blue");
|
||||
assert_eq!(fmt_color(&Color::Cyan).as_str(), "Cyan");
|
||||
assert_eq!(fmt_color(&Color::DarkGray).as_str(), "DarkGray");
|
||||
assert_eq!(fmt_color(&Color::Gray).as_str(), "Gray");
|
||||
assert_eq!(fmt_color(&Color::Green).as_str(), "Green");
|
||||
assert_eq!(fmt_color(&Color::LightBlue).as_str(), "LightBlue");
|
||||
assert_eq!(fmt_color(&Color::LightCyan).as_str(), "LightCyan");
|
||||
assert_eq!(fmt_color(&Color::LightGreen).as_str(), "LightGreen");
|
||||
assert_eq!(fmt_color(&Color::LightMagenta).as_str(), "LightMagenta");
|
||||
assert_eq!(fmt_color(&Color::LightRed).as_str(), "LightRed");
|
||||
assert_eq!(fmt_color(&Color::LightYellow).as_str(), "LightYellow");
|
||||
assert_eq!(fmt_color(&Color::Magenta).as_str(), "Magenta");
|
||||
assert_eq!(fmt_color(&Color::Red).as_str(), "Red");
|
||||
assert_eq!(fmt_color(&Color::Reset).as_str(), "Default");
|
||||
assert_eq!(fmt_color(&Color::White).as_str(), "White");
|
||||
assert_eq!(fmt_color(&Color::Yellow).as_str(), "Yellow");
|
||||
assert_eq!(fmt_color(&Color::Indexed(16)).as_str(), "Default");
|
||||
assert_eq!(fmt_color(&Color::Rgb(204, 170, 22)).as_str(), "#ccaa16");
|
||||
assert_eq!(fmt_color(&Color::Rgb(204, 170, 0)).as_str(), "#ccaa00");
|
||||
// css colors
|
||||
assert_eq!(fmt_color(&Color::Rgb(240, 248, 255)).as_str(), "aliceblue");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(250, 235, 215)).as_str(),
|
||||
"antiquewhite"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 255, 255)).as_str(), "aqua");
|
||||
assert_eq!(fmt_color(&Color::Rgb(127, 255, 212)).as_str(), "aquamarine");
|
||||
assert_eq!(fmt_color(&Color::Rgb(240, 255, 255)).as_str(), "azure");
|
||||
assert_eq!(fmt_color(&Color::Rgb(245, 245, 220)).as_str(), "beige");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 228, 196)).as_str(), "bisque");
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 0, 0)).as_str(), "black");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(255, 235, 205)).as_str(),
|
||||
"blanchedalmond"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 0, 255)).as_str(), "blue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(138, 43, 226)).as_str(), "blueviolet");
|
||||
assert_eq!(fmt_color(&Color::Rgb(165, 42, 42)).as_str(), "brown");
|
||||
assert_eq!(fmt_color(&Color::Rgb(222, 184, 135)).as_str(), "burlywood");
|
||||
assert_eq!(fmt_color(&Color::Rgb(95, 158, 160)).as_str(), "cadetblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(127, 255, 0)).as_str(), "chartreuse");
|
||||
assert_eq!(fmt_color(&Color::Rgb(210, 105, 30)).as_str(), "chocolate");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 127, 80)).as_str(), "coral");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(100, 149, 237)).as_str(),
|
||||
"cornflowerblue"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 248, 220)).as_str(), "cornsilk");
|
||||
assert_eq!(fmt_color(&Color::Rgb(220, 20, 60)).as_str(), "crimson");
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 0, 139)).as_str(), "darkblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 139, 139)).as_str(), "darkcyan");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(184, 134, 11)).as_str(),
|
||||
"darkgoldenrod"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(169, 169, 169)).as_str(), "darkgray");
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 100, 0)).as_str(), "darkgreen");
|
||||
assert_eq!(fmt_color(&Color::Rgb(189, 183, 107)).as_str(), "darkkhaki");
|
||||
assert_eq!(fmt_color(&Color::Rgb(139, 0, 139)).as_str(), "darkmagenta");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(85, 107, 47)).as_str(),
|
||||
"darkolivegreen"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 140, 0)).as_str(), "darkorange");
|
||||
assert_eq!(fmt_color(&Color::Rgb(153, 50, 204)).as_str(), "darkorchid");
|
||||
assert_eq!(fmt_color(&Color::Rgb(139, 0, 0)).as_str(), "darkred");
|
||||
assert_eq!(fmt_color(&Color::Rgb(233, 150, 122)).as_str(), "darksalmon");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(143, 188, 143)).as_str(),
|
||||
"darkseagreen"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(72, 61, 139)).as_str(),
|
||||
"darkslateblue"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(47, 79, 79)).as_str(), "darkslategray");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(0, 206, 209)).as_str(),
|
||||
"darkturquoise"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(148, 0, 211)).as_str(), "darkviolet");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 20, 147)).as_str(), "deeppink");
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 191, 255)).as_str(), "deepskyblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(105, 105, 105)).as_str(), "dimgray");
|
||||
assert_eq!(fmt_color(&Color::Rgb(30, 144, 255)).as_str(), "dodgerblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(178, 34, 34)).as_str(), "firebrick");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(255, 250, 240)).as_str(),
|
||||
"floralwhite"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(34, 139, 34)).as_str(), "forestgreen");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 0, 255)).as_str(), "fuchsia");
|
||||
assert_eq!(fmt_color(&Color::Rgb(220, 220, 220)).as_str(), "gainsboro");
|
||||
assert_eq!(fmt_color(&Color::Rgb(248, 248, 255)).as_str(), "ghostwhite");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 215, 0)).as_str(), "gold");
|
||||
assert_eq!(fmt_color(&Color::Rgb(218, 165, 32)).as_str(), "goldenrod");
|
||||
assert_eq!(fmt_color(&Color::Rgb(128, 128, 128)).as_str(), "gray");
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 128, 0)).as_str(), "green");
|
||||
assert_eq!(fmt_color(&Color::Rgb(173, 255, 47)).as_str(), "greenyellow");
|
||||
assert_eq!(fmt_color(&Color::Rgb(240, 255, 240)).as_str(), "honeydew");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 105, 180)).as_str(), "hotpink");
|
||||
assert_eq!(fmt_color(&Color::Rgb(205, 92, 92)).as_str(), "indianred");
|
||||
assert_eq!(fmt_color(&Color::Rgb(75, 0, 130)).as_str(), "indigo");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 255, 240)).as_str(), "ivory");
|
||||
assert_eq!(fmt_color(&Color::Rgb(240, 230, 140)).as_str(), "khaki");
|
||||
assert_eq!(fmt_color(&Color::Rgb(230, 230, 250)).as_str(), "lavender");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(255, 240, 245)).as_str(),
|
||||
"lavenderblush"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(124, 252, 0)).as_str(), "lawngreen");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(255, 250, 205)).as_str(),
|
||||
"lemonchiffon"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(173, 216, 230)).as_str(), "lightblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(240, 128, 128)).as_str(), "lightcoral");
|
||||
assert_eq!(fmt_color(&Color::Rgb(224, 255, 255)).as_str(), "lightcyan");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(250, 250, 210)).as_str(),
|
||||
"lightgoldenrodyellow"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(211, 211, 211)).as_str(), "lightgray");
|
||||
assert_eq!(fmt_color(&Color::Rgb(144, 238, 144)).as_str(), "lightgreen");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 182, 193)).as_str(), "lightpink");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(255, 160, 122)).as_str(),
|
||||
"lightsalmon"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(32, 178, 170)).as_str(),
|
||||
"lightseagreen"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(135, 206, 250)).as_str(),
|
||||
"lightskyblue"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(119, 136, 153)).as_str(),
|
||||
"lightslategray"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(176, 196, 222)).as_str(),
|
||||
"lightsteelblue"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(255, 255, 224)).as_str(),
|
||||
"lightyellow"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 255, 0)).as_str(), "lime");
|
||||
assert_eq!(fmt_color(&Color::Rgb(50, 205, 50)).as_str(), "limegreen");
|
||||
assert_eq!(fmt_color(&Color::Rgb(250, 240, 230)).as_str(), "linen");
|
||||
assert_eq!(fmt_color(&Color::Rgb(128, 0, 0)).as_str(), "maroon");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(102, 205, 170)).as_str(),
|
||||
"mediumaquamarine"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 0, 205)).as_str(), "mediumblue");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(186, 85, 211)).as_str(),
|
||||
"mediumorchid"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(147, 112, 219)).as_str(),
|
||||
"mediumpurple"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(60, 179, 113)).as_str(),
|
||||
"mediumseagreen"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(123, 104, 238)).as_str(),
|
||||
"mediumslateblue"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(0, 250, 154)).as_str(),
|
||||
"mediumspringgreen"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(72, 209, 204)).as_str(),
|
||||
"mediumturquoise"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(199, 21, 133)).as_str(),
|
||||
"mediumvioletred"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(25, 25, 112)).as_str(), "midnightblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(245, 255, 250)).as_str(), "mintcream");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 228, 225)).as_str(), "mistyrose");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 228, 181)).as_str(), "moccasin");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(255, 222, 173)).as_str(),
|
||||
"navajowhite"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 0, 128)).as_str(), "navy");
|
||||
assert_eq!(fmt_color(&Color::Rgb(253, 245, 230)).as_str(), "oldlace");
|
||||
assert_eq!(fmt_color(&Color::Rgb(128, 128, 0)).as_str(), "olive");
|
||||
assert_eq!(fmt_color(&Color::Rgb(107, 142, 35)).as_str(), "olivedrab");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 165, 0)).as_str(), "orange");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 69, 0)).as_str(), "orangered");
|
||||
assert_eq!(fmt_color(&Color::Rgb(218, 112, 214)).as_str(), "orchid");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(238, 232, 170)).as_str(),
|
||||
"palegoldenrod"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(152, 251, 152)).as_str(), "palegreen");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(175, 238, 238)).as_str(),
|
||||
"paleturquoise"
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(219, 112, 147)).as_str(),
|
||||
"palevioletred"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 239, 213)).as_str(), "papayawhip");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 218, 185)).as_str(), "peachpuff");
|
||||
assert_eq!(fmt_color(&Color::Rgb(205, 133, 63)).as_str(), "peru");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 192, 203)).as_str(), "pink");
|
||||
assert_eq!(fmt_color(&Color::Rgb(221, 160, 221)).as_str(), "plum");
|
||||
assert_eq!(fmt_color(&Color::Rgb(176, 224, 230)).as_str(), "powderblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(128, 0, 128)).as_str(), "purple");
|
||||
assert_eq!(
|
||||
fmt_color(&Color::Rgb(102, 51, 153)).as_str(),
|
||||
"rebeccapurple"
|
||||
);
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 0, 0)).as_str(), "red");
|
||||
assert_eq!(fmt_color(&Color::Rgb(188, 143, 143)).as_str(), "rosybrown");
|
||||
assert_eq!(fmt_color(&Color::Rgb(65, 105, 225)).as_str(), "royalblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(139, 69, 19)).as_str(), "saddlebrown");
|
||||
assert_eq!(fmt_color(&Color::Rgb(250, 128, 114)).as_str(), "salmon");
|
||||
assert_eq!(fmt_color(&Color::Rgb(244, 164, 96)).as_str(), "sandybrown");
|
||||
assert_eq!(fmt_color(&Color::Rgb(46, 139, 87)).as_str(), "seagreen");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 245, 238)).as_str(), "seashell");
|
||||
assert_eq!(fmt_color(&Color::Rgb(160, 82, 45)).as_str(), "sienna");
|
||||
assert_eq!(fmt_color(&Color::Rgb(192, 192, 192)).as_str(), "silver");
|
||||
assert_eq!(fmt_color(&Color::Rgb(135, 206, 235)).as_str(), "skyblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(106, 90, 205)).as_str(), "slateblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(112, 128, 144)).as_str(), "slategray");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 250, 250)).as_str(), "snow");
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 255, 127)).as_str(), "springgreen");
|
||||
assert_eq!(fmt_color(&Color::Rgb(70, 130, 180)).as_str(), "steelblue");
|
||||
assert_eq!(fmt_color(&Color::Rgb(210, 180, 140)).as_str(), "tan");
|
||||
assert_eq!(fmt_color(&Color::Rgb(0, 128, 128)).as_str(), "teal");
|
||||
assert_eq!(fmt_color(&Color::Rgb(216, 191, 216)).as_str(), "thistle");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 99, 71)).as_str(), "tomato");
|
||||
assert_eq!(fmt_color(&Color::Rgb(64, 224, 208)).as_str(), "turquoise");
|
||||
assert_eq!(fmt_color(&Color::Rgb(238, 130, 238)).as_str(), "violet");
|
||||
assert_eq!(fmt_color(&Color::Rgb(245, 222, 179)).as_str(), "wheat");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 255, 255)).as_str(), "white");
|
||||
assert_eq!(fmt_color(&Color::Rgb(245, 245, 245)).as_str(), "whitesmoke");
|
||||
assert_eq!(fmt_color(&Color::Rgb(255, 255, 0)).as_str(), "yellow");
|
||||
assert_eq!(fmt_color(&Color::Rgb(154, 205, 50)).as_str(), "yellowgreen");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -25,45 +25,44 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
extern crate ureq;
|
||||
// Locals
|
||||
use super::parser::parse_semver;
|
||||
// Others
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TagInfo {
|
||||
tag_name: String,
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// ## GithubTag
|
||||
///
|
||||
/// Info related to a github tag
|
||||
pub struct GithubTag {
|
||||
pub tag_name: String,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
/// ### check_for_updates
|
||||
///
|
||||
/// Check if there is a new version available for termscp.
|
||||
/// This is performed through the Github API
|
||||
/// In case of success returns Ok(Option<String>), where the Option is Some(new_version); otherwise if no version is available, return None
|
||||
/// In case of success returns Ok(Option<GithubTag>), where the Option is Some(new_version); otherwise if no version is available, return None
|
||||
/// In case of error returns Error with the error description
|
||||
|
||||
pub fn check_for_updates(current_version: &str) -> Result<Option<String>, String> {
|
||||
pub fn check_for_updates(current_version: &str) -> Result<Option<GithubTag>, String> {
|
||||
// Send request
|
||||
let github_version: Result<String, String> =
|
||||
let github_tag: Result<GithubTag, String> =
|
||||
match ureq::get("https://api.github.com/repos/veeso/termscp/releases/latest").call() {
|
||||
Ok(response) => match response.into_json::<TagInfo>() {
|
||||
Ok(tag_info) => Ok(tag_info.tag_name),
|
||||
Err(err) => Err(err.to_string()),
|
||||
},
|
||||
Ok(response) => response.into_json::<GithubTag>().map_err(|x| x.to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
// Check version
|
||||
match github_version {
|
||||
match github_tag {
|
||||
Err(err) => Err(err),
|
||||
Ok(version) => {
|
||||
Ok(tag) => {
|
||||
// Parse version
|
||||
match parse_semver(version.as_str()) {
|
||||
match parse_semver(tag.tag_name.as_str()) {
|
||||
Some(new_version) => {
|
||||
// Check if version is different
|
||||
if new_version.as_str() > current_version {
|
||||
Ok(Some(new_version)) // New version is available
|
||||
Ok(Some(tag)) // New version is available
|
||||
} else {
|
||||
Ok(None) // No new version
|
||||
}
|
||||
@@ -80,7 +79,15 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(not(all(target_os = "macos", feature = "github-actions")))]
|
||||
#[cfg(not(all(
|
||||
any(
|
||||
target_os = "macos",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "netbsd"
|
||||
),
|
||||
feature = "github-actions"
|
||||
)))]
|
||||
fn test_utils_git_check_for_updates() {
|
||||
assert!(check_for_updates("100.0.0").ok().unwrap().is_none());
|
||||
assert!(check_for_updates("0.0.1").ok().unwrap().is_some());
|
||||
|
||||
@@ -25,13 +25,8 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Dependencies
|
||||
extern crate chrono;
|
||||
extern crate regex;
|
||||
extern crate whoami;
|
||||
|
||||
// Locals
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
#[cfg(not(test))] // NOTE: don't use configuration during tests
|
||||
use crate::system::config_client::ConfigClient;
|
||||
#[cfg(not(test))] // NOTE: don't use configuration during tests
|
||||
@@ -44,6 +39,7 @@ use regex::Regex;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tuirealm::tui::style::Color;
|
||||
|
||||
// Regex
|
||||
lazy_static! {
|
||||
@@ -63,14 +59,20 @@ lazy_static! {
|
||||
* v0.4.0 => 0.4.0
|
||||
*/
|
||||
static ref SEMVER_REGEX: Regex = Regex::new(r".*(:?[0-9]\.[0-9]\.[0-9])").unwrap();
|
||||
}
|
||||
|
||||
pub struct RemoteOptions {
|
||||
pub hostname: String,
|
||||
pub port: u16,
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub username: Option<String>,
|
||||
pub wrkdir: Option<PathBuf>,
|
||||
/**
|
||||
* Regex matches:
|
||||
* - group 1: Red
|
||||
* - group 2: Green
|
||||
* - group 3: Blue
|
||||
*/
|
||||
static ref COLOR_HEX_REGEX: Regex = Regex::new(r"#(:?[0-9a-fA-F]{2})(:?[0-9a-fA-F]{2})(:?[0-9a-fA-F]{2})").unwrap();
|
||||
/**
|
||||
* Regex matches:
|
||||
* - group 2: Red
|
||||
* - group 4: Green
|
||||
* - group 6: blue
|
||||
*/
|
||||
static ref COLOR_RGB_REGEX: Regex = Regex::new(r"^(rgb)?\(?([01]?\d\d?|2[0-4]\d|25[0-5])(\W+)([01]?\d\d?|2[0-4]\d|25[0-5])\W+(([01]?\d\d?|2[0-4]\d|25[0-5])\)?)").unwrap();
|
||||
}
|
||||
|
||||
/// ### parse_remote_opt
|
||||
@@ -91,8 +93,7 @@ pub struct RemoteOptions {
|
||||
/// - sftp://172.26.104.1:4022
|
||||
/// - sftp://172.26.104.1
|
||||
/// - ...
|
||||
///
|
||||
pub fn parse_remote_opt(remote: &str) -> Result<RemoteOptions, String> {
|
||||
pub fn parse_remote_opt(remote: &str) -> Result<FileTransferParams, String> {
|
||||
// Set protocol to default protocol
|
||||
#[cfg(not(test))] // NOTE: don't use configuration during tests
|
||||
let mut protocol: FileTransferProtocol = match environment::init_config_dir() {
|
||||
@@ -142,7 +143,7 @@ pub fn parse_remote_opt(remote: &str) -> Result<RemoteOptions, String> {
|
||||
},
|
||||
};
|
||||
// Get address
|
||||
let hostname: String = match groups.get(3) {
|
||||
let address: String = match groups.get(3) {
|
||||
Some(group) => group.as_str().to_string(),
|
||||
None => return Err(String::from("Missing address")),
|
||||
};
|
||||
@@ -154,14 +155,13 @@ pub fn parse_remote_opt(remote: &str) -> Result<RemoteOptions, String> {
|
||||
};
|
||||
}
|
||||
// Get workdir
|
||||
let wrkdir: Option<PathBuf> = groups.get(5).map(|group| PathBuf::from(group.as_str()));
|
||||
Ok(RemoteOptions {
|
||||
hostname,
|
||||
port,
|
||||
protocol,
|
||||
username,
|
||||
wrkdir,
|
||||
})
|
||||
let entry_directory: Option<PathBuf> =
|
||||
groups.get(5).map(|group| PathBuf::from(group.as_str()));
|
||||
Ok(FileTransferParams::new(address)
|
||||
.port(port)
|
||||
.protocol(protocol)
|
||||
.username(username)
|
||||
.entry_directory(entry_directory))
|
||||
}
|
||||
None => Err(String::from("Bad remote host syntax!")),
|
||||
}
|
||||
@@ -224,6 +224,237 @@ pub fn parse_semver(haystack: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### parse_color
|
||||
///
|
||||
/// Parse color from string into a `Color` enum.
|
||||
///
|
||||
/// Color may be in different format:
|
||||
///
|
||||
/// 1. color name:
|
||||
/// - Black,
|
||||
/// - Blue,
|
||||
/// - Cyan,
|
||||
/// - DarkGray,
|
||||
/// - Gray,
|
||||
/// - Green,
|
||||
/// - LightBlue,
|
||||
/// - LightCyan,
|
||||
/// - LightGreen,
|
||||
/// - LightMagenta,
|
||||
/// - LightRed,
|
||||
/// - LightYellow,
|
||||
/// - Magenta,
|
||||
/// - Red,
|
||||
/// - Reset,
|
||||
/// - White,
|
||||
/// - Yellow,
|
||||
/// 2. Hex format:
|
||||
/// - #f0ab05
|
||||
/// - #AA33BC
|
||||
/// 3. Rgb format:
|
||||
/// - rgb(255, 64, 32)
|
||||
/// - rgb(255,64,32)
|
||||
/// - 255, 64, 32
|
||||
pub fn parse_color(color: &str) -> Option<Color> {
|
||||
match color.to_lowercase().as_str() {
|
||||
// -- lib colors
|
||||
"black" => Some(Color::Black),
|
||||
"blue" => Some(Color::Blue),
|
||||
"cyan" => Some(Color::Cyan),
|
||||
"darkgray" | "darkgrey" => Some(Color::DarkGray),
|
||||
"default" => Some(Color::Reset),
|
||||
"gray" => Some(Color::Gray),
|
||||
"green" => Some(Color::Green),
|
||||
"lightblue" => Some(Color::LightBlue),
|
||||
"lightcyan" => Some(Color::LightCyan),
|
||||
"lightgreen" => Some(Color::LightGreen),
|
||||
"lightmagenta" => Some(Color::LightMagenta),
|
||||
"lightred" => Some(Color::LightRed),
|
||||
"lightyellow" => Some(Color::LightYellow),
|
||||
"magenta" => Some(Color::Magenta),
|
||||
"red" => Some(Color::Red),
|
||||
"white" => Some(Color::White),
|
||||
"yellow" => Some(Color::Yellow),
|
||||
// -- css colors
|
||||
"aliceblue" => Some(Color::Rgb(240, 248, 255)),
|
||||
"antiquewhite" => Some(Color::Rgb(250, 235, 215)),
|
||||
"aqua" => Some(Color::Rgb(0, 255, 255)),
|
||||
"aquamarine" => Some(Color::Rgb(127, 255, 212)),
|
||||
"azure" => Some(Color::Rgb(240, 255, 255)),
|
||||
"beige" => Some(Color::Rgb(245, 245, 220)),
|
||||
"bisque" => Some(Color::Rgb(255, 228, 196)),
|
||||
"blanchedalmond" => Some(Color::Rgb(255, 235, 205)),
|
||||
"blueviolet" => Some(Color::Rgb(138, 43, 226)),
|
||||
"brown" => Some(Color::Rgb(165, 42, 42)),
|
||||
"burlywood" => Some(Color::Rgb(222, 184, 135)),
|
||||
"cadetblue" => Some(Color::Rgb(95, 158, 160)),
|
||||
"chartreuse" => Some(Color::Rgb(127, 255, 0)),
|
||||
"chocolate" => Some(Color::Rgb(210, 105, 30)),
|
||||
"coral" => Some(Color::Rgb(255, 127, 80)),
|
||||
"cornflowerblue" => Some(Color::Rgb(100, 149, 237)),
|
||||
"cornsilk" => Some(Color::Rgb(255, 248, 220)),
|
||||
"crimson" => Some(Color::Rgb(220, 20, 60)),
|
||||
"darkblue" => Some(Color::Rgb(0, 0, 139)),
|
||||
"darkcyan" => Some(Color::Rgb(0, 139, 139)),
|
||||
"darkgoldenrod" => Some(Color::Rgb(184, 134, 11)),
|
||||
"darkgreen" => Some(Color::Rgb(0, 100, 0)),
|
||||
"darkkhaki" => Some(Color::Rgb(189, 183, 107)),
|
||||
"darkmagenta" => Some(Color::Rgb(139, 0, 139)),
|
||||
"darkolivegreen" => Some(Color::Rgb(85, 107, 47)),
|
||||
"darkorange" => Some(Color::Rgb(255, 140, 0)),
|
||||
"darkorchid" => Some(Color::Rgb(153, 50, 204)),
|
||||
"darkred" => Some(Color::Rgb(139, 0, 0)),
|
||||
"darksalmon" => Some(Color::Rgb(233, 150, 122)),
|
||||
"darkseagreen" => Some(Color::Rgb(143, 188, 143)),
|
||||
"darkslateblue" => Some(Color::Rgb(72, 61, 139)),
|
||||
"darkslategray" | "darkslategrey" => Some(Color::Rgb(47, 79, 79)),
|
||||
"darkturquoise" => Some(Color::Rgb(0, 206, 209)),
|
||||
"darkviolet" => Some(Color::Rgb(148, 0, 211)),
|
||||
"deeppink" => Some(Color::Rgb(255, 20, 147)),
|
||||
"deepskyblue" => Some(Color::Rgb(0, 191, 255)),
|
||||
"dimgray" | "dimgrey" => Some(Color::Rgb(105, 105, 105)),
|
||||
"dodgerblue" => Some(Color::Rgb(30, 144, 255)),
|
||||
"firebrick" => Some(Color::Rgb(178, 34, 34)),
|
||||
"floralwhite" => Some(Color::Rgb(255, 250, 240)),
|
||||
"forestgreen" => Some(Color::Rgb(34, 139, 34)),
|
||||
"fuchsia" => Some(Color::Rgb(255, 0, 255)),
|
||||
"gainsboro" => Some(Color::Rgb(220, 220, 220)),
|
||||
"ghostwhite" => Some(Color::Rgb(248, 248, 255)),
|
||||
"gold" => Some(Color::Rgb(255, 215, 0)),
|
||||
"goldenrod" => Some(Color::Rgb(218, 165, 32)),
|
||||
"greenyellow" => Some(Color::Rgb(173, 255, 47)),
|
||||
"grey" => Some(Color::Rgb(128, 128, 128)),
|
||||
"honeydew" => Some(Color::Rgb(240, 255, 240)),
|
||||
"hotpink" => Some(Color::Rgb(255, 105, 180)),
|
||||
"indianred" => Some(Color::Rgb(205, 92, 92)),
|
||||
"indigo" => Some(Color::Rgb(75, 0, 130)),
|
||||
"ivory" => Some(Color::Rgb(255, 255, 240)),
|
||||
"khaki" => Some(Color::Rgb(240, 230, 140)),
|
||||
"lavender" => Some(Color::Rgb(230, 230, 250)),
|
||||
"lavenderblush" => Some(Color::Rgb(255, 240, 245)),
|
||||
"lawngreen" => Some(Color::Rgb(124, 252, 0)),
|
||||
"lemonchiffon" => Some(Color::Rgb(255, 250, 205)),
|
||||
"lightcoral" => Some(Color::Rgb(240, 128, 128)),
|
||||
"lightgoldenrodyellow" => Some(Color::Rgb(250, 250, 210)),
|
||||
"lightgray" | "lightgrey" => Some(Color::Rgb(211, 211, 211)),
|
||||
"lightpink" => Some(Color::Rgb(255, 182, 193)),
|
||||
"lightsalmon" => Some(Color::Rgb(255, 160, 122)),
|
||||
"lightseagreen" => Some(Color::Rgb(32, 178, 170)),
|
||||
"lightskyblue" => Some(Color::Rgb(135, 206, 250)),
|
||||
"lightslategray" | "lightslategrey" => Some(Color::Rgb(119, 136, 153)),
|
||||
"lightsteelblue" => Some(Color::Rgb(176, 196, 222)),
|
||||
"lime" => Some(Color::Rgb(0, 255, 0)),
|
||||
"limegreen" => Some(Color::Rgb(50, 205, 50)),
|
||||
"linen" => Some(Color::Rgb(250, 240, 230)),
|
||||
"maroon" => Some(Color::Rgb(128, 0, 0)),
|
||||
"mediumaquamarine" => Some(Color::Rgb(102, 205, 170)),
|
||||
"mediumblue" => Some(Color::Rgb(0, 0, 205)),
|
||||
"mediumorchid" => Some(Color::Rgb(186, 85, 211)),
|
||||
"mediumpurple" => Some(Color::Rgb(147, 112, 219)),
|
||||
"mediumseagreen" => Some(Color::Rgb(60, 179, 113)),
|
||||
"mediumslateblue" => Some(Color::Rgb(123, 104, 238)),
|
||||
"mediumspringgreen" => Some(Color::Rgb(0, 250, 154)),
|
||||
"mediumturquoise" => Some(Color::Rgb(72, 209, 204)),
|
||||
"mediumvioletred" => Some(Color::Rgb(199, 21, 133)),
|
||||
"midnightblue" => Some(Color::Rgb(25, 25, 112)),
|
||||
"mintcream" => Some(Color::Rgb(245, 255, 250)),
|
||||
"mistyrose" => Some(Color::Rgb(255, 228, 225)),
|
||||
"moccasin" => Some(Color::Rgb(255, 228, 181)),
|
||||
"navajowhite" => Some(Color::Rgb(255, 222, 173)),
|
||||
"navy" => Some(Color::Rgb(0, 0, 128)),
|
||||
"oldlace" => Some(Color::Rgb(253, 245, 230)),
|
||||
"olive" => Some(Color::Rgb(128, 128, 0)),
|
||||
"olivedrab" => Some(Color::Rgb(107, 142, 35)),
|
||||
"orange" => Some(Color::Rgb(255, 165, 0)),
|
||||
"orangered" => Some(Color::Rgb(255, 69, 0)),
|
||||
"orchid" => Some(Color::Rgb(218, 112, 214)),
|
||||
"palegoldenrod" => Some(Color::Rgb(238, 232, 170)),
|
||||
"palegreen" => Some(Color::Rgb(152, 251, 152)),
|
||||
"paleturquoise" => Some(Color::Rgb(175, 238, 238)),
|
||||
"palevioletred" => Some(Color::Rgb(219, 112, 147)),
|
||||
"papayawhip" => Some(Color::Rgb(255, 239, 213)),
|
||||
"peachpuff" => Some(Color::Rgb(255, 218, 185)),
|
||||
"peru" => Some(Color::Rgb(205, 133, 63)),
|
||||
"pink" => Some(Color::Rgb(255, 192, 203)),
|
||||
"plum" => Some(Color::Rgb(221, 160, 221)),
|
||||
"powderblue" => Some(Color::Rgb(176, 224, 230)),
|
||||
"purple" => Some(Color::Rgb(128, 0, 128)),
|
||||
"rebeccapurple" => Some(Color::Rgb(102, 51, 153)),
|
||||
"rosybrown" => Some(Color::Rgb(188, 143, 143)),
|
||||
"royalblue" => Some(Color::Rgb(65, 105, 225)),
|
||||
"saddlebrown" => Some(Color::Rgb(139, 69, 19)),
|
||||
"salmon" => Some(Color::Rgb(250, 128, 114)),
|
||||
"sandybrown" => Some(Color::Rgb(244, 164, 96)),
|
||||
"seagreen" => Some(Color::Rgb(46, 139, 87)),
|
||||
"seashell" => Some(Color::Rgb(255, 245, 238)),
|
||||
"sienna" => Some(Color::Rgb(160, 82, 45)),
|
||||
"silver" => Some(Color::Rgb(192, 192, 192)),
|
||||
"skyblue" => Some(Color::Rgb(135, 206, 235)),
|
||||
"slateblue" => Some(Color::Rgb(106, 90, 205)),
|
||||
"slategray" | "slategrey" => Some(Color::Rgb(112, 128, 144)),
|
||||
"snow" => Some(Color::Rgb(255, 250, 250)),
|
||||
"springgreen" => Some(Color::Rgb(0, 255, 127)),
|
||||
"steelblue" => Some(Color::Rgb(70, 130, 180)),
|
||||
"tan" => Some(Color::Rgb(210, 180, 140)),
|
||||
"teal" => Some(Color::Rgb(0, 128, 128)),
|
||||
"thistle" => Some(Color::Rgb(216, 191, 216)),
|
||||
"tomato" => Some(Color::Rgb(255, 99, 71)),
|
||||
"turquoise" => Some(Color::Rgb(64, 224, 208)),
|
||||
"violet" => Some(Color::Rgb(238, 130, 238)),
|
||||
"wheat" => Some(Color::Rgb(245, 222, 179)),
|
||||
"whitesmoke" => Some(Color::Rgb(245, 245, 245)),
|
||||
"yellowgreen" => Some(Color::Rgb(154, 205, 50)),
|
||||
// -- hex and rgb
|
||||
other => {
|
||||
// Try as hex
|
||||
if let Some(color) = parse_hex_color(other) {
|
||||
Some(color)
|
||||
} else {
|
||||
parse_rgb_color(other)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### parse_hex_color
|
||||
///
|
||||
/// Try to parse a color in hex format, such as:
|
||||
///
|
||||
/// - #f0ab05
|
||||
/// - #AA33BC
|
||||
fn parse_hex_color(color: &str) -> Option<Color> {
|
||||
COLOR_HEX_REGEX.captures(color).map(|groups| {
|
||||
Color::Rgb(
|
||||
u8::from_str_radix(groups.get(1).unwrap().as_str(), 16)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
u8::from_str_radix(groups.get(2).unwrap().as_str(), 16)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
u8::from_str_radix(groups.get(3).unwrap().as_str(), 16)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// ### parse_rgb_color
|
||||
///
|
||||
/// Try to parse a color in rgb format, such as:
|
||||
///
|
||||
/// - rgb(255, 64, 32)
|
||||
/// - rgb(255,64,32)
|
||||
/// - 255, 64, 32
|
||||
fn parse_rgb_color(color: &str) -> Option<Color> {
|
||||
COLOR_RGB_REGEX.captures(color).map(|groups| {
|
||||
Color::Rgb(
|
||||
u8::from_str(groups.get(2).unwrap().as_str()).ok().unwrap(),
|
||||
u8::from_str(groups.get(4).unwrap().as_str()).ok().unwrap(),
|
||||
u8::from_str(groups.get(6).unwrap().as_str()).ok().unwrap(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -235,107 +466,110 @@ mod tests {
|
||||
#[test]
|
||||
fn test_utils_parse_remote_opt() {
|
||||
// Base case
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1"))
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some());
|
||||
// User case
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1"))
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.username.unwrap(), String::from("root"));
|
||||
assert!(result.wrkdir.is_none());
|
||||
assert!(result.entry_directory.is_none());
|
||||
// User + port
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 8022);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.username.unwrap(), String::from("root"));
|
||||
assert!(result.wrkdir.is_none());
|
||||
assert!(result.entry_directory.is_none());
|
||||
// Port only
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1:4022"))
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:4022"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 4022);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some());
|
||||
assert!(result.wrkdir.is_none());
|
||||
assert!(result.entry_directory.is_none());
|
||||
// Protocol
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("ftp://172.26.104.1"))
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("ftp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 21); // Fallback to ftp default
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
|
||||
assert!(result.username.is_none()); // Doesn't fall back
|
||||
assert!(result.wrkdir.is_none());
|
||||
assert!(result.entry_directory.is_none());
|
||||
// Protocol
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("sftp://172.26.104.1"))
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("sftp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22); // Fallback to sftp default
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some()); // Doesn't fall back
|
||||
assert!(result.wrkdir.is_none());
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("scp://172.26.104.1"))
|
||||
assert!(result.entry_directory.is_none());
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("scp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22); // Fallback to scp default
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Scp);
|
||||
assert!(result.username.is_some()); // Doesn't fall back
|
||||
assert!(result.wrkdir.is_none());
|
||||
assert!(result.entry_directory.is_none());
|
||||
// Protocol + user
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
let result: FileTransferParams =
|
||||
parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 21); // Fallback to ftp default
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Ftp(true));
|
||||
assert_eq!(result.username.unwrap(), String::from("anon"));
|
||||
assert!(result.wrkdir.is_none());
|
||||
assert!(result.entry_directory.is_none());
|
||||
// Path
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
let result: FileTransferParams =
|
||||
parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 8022);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.username.unwrap(), String::from("root"));
|
||||
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("/var"));
|
||||
assert_eq!(result.entry_directory.unwrap(), PathBuf::from("/var"));
|
||||
// Port only
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1:home"))
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:home"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some());
|
||||
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("home"));
|
||||
assert_eq!(result.entry_directory.unwrap(), PathBuf::from("home"));
|
||||
// All together now
|
||||
let result: RemoteOptions =
|
||||
let result: FileTransferParams =
|
||||
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 8021); // Fallback to ftp default
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
|
||||
assert_eq!(result.username.unwrap(), String::from("anon"));
|
||||
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("/tmp"));
|
||||
assert_eq!(result.entry_directory.unwrap(), PathBuf::from("/tmp"));
|
||||
// bad syntax
|
||||
assert!(parse_remote_opt(&String::from("omar://172.26.104.1")).is_err()); // Bad protocol
|
||||
assert!(parse_remote_opt(&String::from("omar://172.26.104.1:650000")).is_err());
|
||||
// Bad protocol
|
||||
assert!(parse_remote_opt(&String::from("omar://172.26.104.1")).is_err());
|
||||
// Bad port
|
||||
assert!(parse_remote_opt(&String::from("scp://172.26.104.1:650000")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -410,4 +644,245 @@ mod tests {
|
||||
assert_eq!(parse_semver("1.0.0").unwrap(), String::from("1.0.0"),);
|
||||
assert!(parse_semver("v1.1").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_parse_color_hex() {
|
||||
assert_eq!(
|
||||
parse_hex_color("#f0f0f0").unwrap(),
|
||||
Color::Rgb(240, 240, 240)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_hex_color("#60AAcc").unwrap(),
|
||||
Color::Rgb(96, 170, 204)
|
||||
);
|
||||
assert!(parse_hex_color("#fatboy").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_parse_color_rgb() {
|
||||
assert_eq!(
|
||||
parse_rgb_color("rgb(255, 64, 32)").unwrap(),
|
||||
Color::Rgb(255, 64, 32)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_rgb_color("rgb(255,64,32)").unwrap(),
|
||||
Color::Rgb(255, 64, 32)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_rgb_color("(255,64,32)").unwrap(),
|
||||
Color::Rgb(255, 64, 32)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_rgb_color("255,64,32").unwrap(),
|
||||
Color::Rgb(255, 64, 32)
|
||||
);
|
||||
assert!(parse_rgb_color("(300, 128, 512)").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_parse_color() {
|
||||
assert_eq!(parse_color("Black").unwrap(), Color::Black);
|
||||
assert_eq!(parse_color("BLUE").unwrap(), Color::Blue);
|
||||
assert_eq!(parse_color("Cyan").unwrap(), Color::Cyan);
|
||||
assert_eq!(parse_color("DarkGray").unwrap(), Color::DarkGray);
|
||||
assert_eq!(parse_color("Gray").unwrap(), Color::Gray);
|
||||
assert_eq!(parse_color("Green").unwrap(), Color::Green);
|
||||
assert_eq!(parse_color("LightBlue").unwrap(), Color::LightBlue);
|
||||
assert_eq!(parse_color("LightCyan").unwrap(), Color::LightCyan);
|
||||
assert_eq!(parse_color("LightGreen").unwrap(), Color::LightGreen);
|
||||
assert_eq!(parse_color("LightMagenta").unwrap(), Color::LightMagenta);
|
||||
assert_eq!(parse_color("LightRed").unwrap(), Color::LightRed);
|
||||
assert_eq!(parse_color("LightYellow").unwrap(), Color::LightYellow);
|
||||
assert_eq!(parse_color("Magenta").unwrap(), Color::Magenta);
|
||||
assert_eq!(parse_color("Red").unwrap(), Color::Red);
|
||||
assert_eq!(parse_color("Default").unwrap(), Color::Reset);
|
||||
assert_eq!(parse_color("White").unwrap(), Color::White);
|
||||
assert_eq!(parse_color("Yellow").unwrap(), Color::Yellow);
|
||||
assert_eq!(parse_color("#f0f0f0").unwrap(), Color::Rgb(240, 240, 240));
|
||||
// -- css colors
|
||||
assert_eq!(parse_color("aliceblue"), Some(Color::Rgb(240, 248, 255)));
|
||||
assert_eq!(parse_color("antiquewhite"), Some(Color::Rgb(250, 235, 215)));
|
||||
assert_eq!(parse_color("aqua"), Some(Color::Rgb(0, 255, 255)));
|
||||
assert_eq!(parse_color("aquamarine"), Some(Color::Rgb(127, 255, 212)));
|
||||
assert_eq!(parse_color("azure"), Some(Color::Rgb(240, 255, 255)));
|
||||
assert_eq!(parse_color("beige"), Some(Color::Rgb(245, 245, 220)));
|
||||
assert_eq!(parse_color("bisque"), Some(Color::Rgb(255, 228, 196)));
|
||||
assert_eq!(
|
||||
parse_color("blanchedalmond"),
|
||||
Some(Color::Rgb(255, 235, 205))
|
||||
);
|
||||
assert_eq!(parse_color("blueviolet"), Some(Color::Rgb(138, 43, 226)));
|
||||
assert_eq!(parse_color("brown"), Some(Color::Rgb(165, 42, 42)));
|
||||
assert_eq!(parse_color("burlywood"), Some(Color::Rgb(222, 184, 135)));
|
||||
assert_eq!(parse_color("cadetblue"), Some(Color::Rgb(95, 158, 160)));
|
||||
assert_eq!(parse_color("chartreuse"), Some(Color::Rgb(127, 255, 0)));
|
||||
assert_eq!(parse_color("chocolate"), Some(Color::Rgb(210, 105, 30)));
|
||||
assert_eq!(parse_color("coral"), Some(Color::Rgb(255, 127, 80)));
|
||||
assert_eq!(
|
||||
parse_color("cornflowerblue"),
|
||||
Some(Color::Rgb(100, 149, 237))
|
||||
);
|
||||
assert_eq!(parse_color("cornsilk"), Some(Color::Rgb(255, 248, 220)));
|
||||
assert_eq!(parse_color("crimson"), Some(Color::Rgb(220, 20, 60)));
|
||||
assert_eq!(parse_color("darkblue"), Some(Color::Rgb(0, 0, 139)));
|
||||
assert_eq!(parse_color("darkcyan"), Some(Color::Rgb(0, 139, 139)));
|
||||
assert_eq!(parse_color("darkgoldenrod"), Some(Color::Rgb(184, 134, 11)));
|
||||
assert_eq!(parse_color("darkgreen"), Some(Color::Rgb(0, 100, 0)));
|
||||
assert_eq!(parse_color("darkkhaki"), Some(Color::Rgb(189, 183, 107)));
|
||||
assert_eq!(parse_color("darkmagenta"), Some(Color::Rgb(139, 0, 139)));
|
||||
assert_eq!(parse_color("darkolivegreen"), Some(Color::Rgb(85, 107, 47)));
|
||||
assert_eq!(parse_color("darkorange"), Some(Color::Rgb(255, 140, 0)));
|
||||
assert_eq!(parse_color("darkorchid"), Some(Color::Rgb(153, 50, 204)));
|
||||
assert_eq!(parse_color("darkred"), Some(Color::Rgb(139, 0, 0)));
|
||||
assert_eq!(parse_color("darksalmon"), Some(Color::Rgb(233, 150, 122)));
|
||||
assert_eq!(parse_color("darkseagreen"), Some(Color::Rgb(143, 188, 143)));
|
||||
assert_eq!(parse_color("darkslateblue"), Some(Color::Rgb(72, 61, 139)));
|
||||
assert_eq!(parse_color("darkslategray"), Some(Color::Rgb(47, 79, 79)));
|
||||
assert_eq!(parse_color("darkslategrey"), Some(Color::Rgb(47, 79, 79)));
|
||||
assert_eq!(parse_color("darkturquoise"), Some(Color::Rgb(0, 206, 209)));
|
||||
assert_eq!(parse_color("darkviolet"), Some(Color::Rgb(148, 0, 211)));
|
||||
assert_eq!(parse_color("deeppink"), Some(Color::Rgb(255, 20, 147)));
|
||||
assert_eq!(parse_color("deepskyblue"), Some(Color::Rgb(0, 191, 255)));
|
||||
assert_eq!(parse_color("dimgray"), Some(Color::Rgb(105, 105, 105)));
|
||||
assert_eq!(parse_color("dimgrey"), Some(Color::Rgb(105, 105, 105)));
|
||||
assert_eq!(parse_color("dodgerblue"), Some(Color::Rgb(30, 144, 255)));
|
||||
assert_eq!(parse_color("firebrick"), Some(Color::Rgb(178, 34, 34)));
|
||||
assert_eq!(parse_color("floralwhite"), Some(Color::Rgb(255, 250, 240)));
|
||||
assert_eq!(parse_color("forestgreen"), Some(Color::Rgb(34, 139, 34)));
|
||||
assert_eq!(parse_color("fuchsia"), Some(Color::Rgb(255, 0, 255)));
|
||||
assert_eq!(parse_color("gainsboro"), Some(Color::Rgb(220, 220, 220)));
|
||||
assert_eq!(parse_color("ghostwhite"), Some(Color::Rgb(248, 248, 255)));
|
||||
assert_eq!(parse_color("gold"), Some(Color::Rgb(255, 215, 0)));
|
||||
assert_eq!(parse_color("goldenrod"), Some(Color::Rgb(218, 165, 32)));
|
||||
assert_eq!(parse_color("greenyellow"), Some(Color::Rgb(173, 255, 47)));
|
||||
assert_eq!(parse_color("honeydew"), Some(Color::Rgb(240, 255, 240)));
|
||||
assert_eq!(parse_color("hotpink"), Some(Color::Rgb(255, 105, 180)));
|
||||
assert_eq!(parse_color("indianred"), Some(Color::Rgb(205, 92, 92)));
|
||||
assert_eq!(parse_color("indigo"), Some(Color::Rgb(75, 0, 130)));
|
||||
assert_eq!(parse_color("ivory"), Some(Color::Rgb(255, 255, 240)));
|
||||
assert_eq!(parse_color("khaki"), Some(Color::Rgb(240, 230, 140)));
|
||||
assert_eq!(parse_color("lavender"), Some(Color::Rgb(230, 230, 250)));
|
||||
assert_eq!(
|
||||
parse_color("lavenderblush"),
|
||||
Some(Color::Rgb(255, 240, 245))
|
||||
);
|
||||
assert_eq!(parse_color("lawngreen"), Some(Color::Rgb(124, 252, 0)));
|
||||
assert_eq!(parse_color("lemonchiffon"), Some(Color::Rgb(255, 250, 205)));
|
||||
assert_eq!(parse_color("lightcoral"), Some(Color::Rgb(240, 128, 128)));
|
||||
assert_eq!(
|
||||
parse_color("lightgoldenrodyellow"),
|
||||
Some(Color::Rgb(250, 250, 210))
|
||||
);
|
||||
assert_eq!(parse_color("lightpink"), Some(Color::Rgb(255, 182, 193)));
|
||||
assert_eq!(parse_color("lightsalmon"), Some(Color::Rgb(255, 160, 122)));
|
||||
assert_eq!(parse_color("lightseagreen"), Some(Color::Rgb(32, 178, 170)));
|
||||
assert_eq!(parse_color("lightskyblue"), Some(Color::Rgb(135, 206, 250)));
|
||||
assert_eq!(
|
||||
parse_color("lightslategray"),
|
||||
Some(Color::Rgb(119, 136, 153))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_color("lightslategrey"),
|
||||
Some(Color::Rgb(119, 136, 153))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_color("lightsteelblue"),
|
||||
Some(Color::Rgb(176, 196, 222))
|
||||
);
|
||||
assert_eq!(parse_color("lime"), Some(Color::Rgb(0, 255, 0)));
|
||||
assert_eq!(parse_color("limegreen"), Some(Color::Rgb(50, 205, 50)));
|
||||
assert_eq!(parse_color("linen"), Some(Color::Rgb(250, 240, 230)));
|
||||
assert_eq!(parse_color("maroon"), Some(Color::Rgb(128, 0, 0)));
|
||||
assert_eq!(
|
||||
parse_color("mediumaquamarine"),
|
||||
Some(Color::Rgb(102, 205, 170))
|
||||
);
|
||||
assert_eq!(parse_color("mediumblue"), Some(Color::Rgb(0, 0, 205)));
|
||||
assert_eq!(parse_color("mediumorchid"), Some(Color::Rgb(186, 85, 211)));
|
||||
assert_eq!(parse_color("mediumpurple"), Some(Color::Rgb(147, 112, 219)));
|
||||
assert_eq!(
|
||||
parse_color("mediumseagreen"),
|
||||
Some(Color::Rgb(60, 179, 113))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_color("mediumslateblue"),
|
||||
Some(Color::Rgb(123, 104, 238))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_color("mediumspringgreen"),
|
||||
Some(Color::Rgb(0, 250, 154))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_color("mediumturquoise"),
|
||||
Some(Color::Rgb(72, 209, 204))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_color("mediumvioletred"),
|
||||
Some(Color::Rgb(199, 21, 133))
|
||||
);
|
||||
assert_eq!(parse_color("midnightblue"), Some(Color::Rgb(25, 25, 112)));
|
||||
assert_eq!(parse_color("mintcream"), Some(Color::Rgb(245, 255, 250)));
|
||||
assert_eq!(parse_color("mistyrose"), Some(Color::Rgb(255, 228, 225)));
|
||||
assert_eq!(parse_color("moccasin"), Some(Color::Rgb(255, 228, 181)));
|
||||
assert_eq!(parse_color("navajowhite"), Some(Color::Rgb(255, 222, 173)));
|
||||
assert_eq!(parse_color("navy"), Some(Color::Rgb(0, 0, 128)));
|
||||
assert_eq!(parse_color("oldlace"), Some(Color::Rgb(253, 245, 230)));
|
||||
assert_eq!(parse_color("olive"), Some(Color::Rgb(128, 128, 0)));
|
||||
assert_eq!(parse_color("olivedrab"), Some(Color::Rgb(107, 142, 35)));
|
||||
assert_eq!(parse_color("orange"), Some(Color::Rgb(255, 165, 0)));
|
||||
assert_eq!(parse_color("orangered"), Some(Color::Rgb(255, 69, 0)));
|
||||
assert_eq!(parse_color("orchid"), Some(Color::Rgb(218, 112, 214)));
|
||||
assert_eq!(
|
||||
parse_color("palegoldenrod"),
|
||||
Some(Color::Rgb(238, 232, 170))
|
||||
);
|
||||
assert_eq!(parse_color("palegreen"), Some(Color::Rgb(152, 251, 152)));
|
||||
assert_eq!(
|
||||
parse_color("paleturquoise"),
|
||||
Some(Color::Rgb(175, 238, 238))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_color("palevioletred"),
|
||||
Some(Color::Rgb(219, 112, 147))
|
||||
);
|
||||
assert_eq!(parse_color("papayawhip"), Some(Color::Rgb(255, 239, 213)));
|
||||
assert_eq!(parse_color("peachpuff"), Some(Color::Rgb(255, 218, 185)));
|
||||
assert_eq!(parse_color("peru"), Some(Color::Rgb(205, 133, 63)));
|
||||
assert_eq!(parse_color("pink"), Some(Color::Rgb(255, 192, 203)));
|
||||
assert_eq!(parse_color("plum"), Some(Color::Rgb(221, 160, 221)));
|
||||
assert_eq!(parse_color("powderblue"), Some(Color::Rgb(176, 224, 230)));
|
||||
assert_eq!(parse_color("purple"), Some(Color::Rgb(128, 0, 128)));
|
||||
assert_eq!(parse_color("rebeccapurple"), Some(Color::Rgb(102, 51, 153)));
|
||||
assert_eq!(parse_color("rosybrown"), Some(Color::Rgb(188, 143, 143)));
|
||||
assert_eq!(parse_color("royalblue"), Some(Color::Rgb(65, 105, 225)));
|
||||
assert_eq!(parse_color("saddlebrown"), Some(Color::Rgb(139, 69, 19)));
|
||||
assert_eq!(parse_color("salmon"), Some(Color::Rgb(250, 128, 114)));
|
||||
assert_eq!(parse_color("sandybrown"), Some(Color::Rgb(244, 164, 96)));
|
||||
assert_eq!(parse_color("seagreen"), Some(Color::Rgb(46, 139, 87)));
|
||||
assert_eq!(parse_color("seashell"), Some(Color::Rgb(255, 245, 238)));
|
||||
assert_eq!(parse_color("sienna"), Some(Color::Rgb(160, 82, 45)));
|
||||
assert_eq!(parse_color("silver"), Some(Color::Rgb(192, 192, 192)));
|
||||
assert_eq!(parse_color("skyblue"), Some(Color::Rgb(135, 206, 235)));
|
||||
assert_eq!(parse_color("slateblue"), Some(Color::Rgb(106, 90, 205)));
|
||||
assert_eq!(parse_color("slategray"), Some(Color::Rgb(112, 128, 144)));
|
||||
assert_eq!(parse_color("slategrey"), Some(Color::Rgb(112, 128, 144)));
|
||||
assert_eq!(parse_color("snow"), Some(Color::Rgb(255, 250, 250)));
|
||||
assert_eq!(parse_color("springgreen"), Some(Color::Rgb(0, 255, 127)));
|
||||
assert_eq!(parse_color("steelblue"), Some(Color::Rgb(70, 130, 180)));
|
||||
assert_eq!(parse_color("tan"), Some(Color::Rgb(210, 180, 140)));
|
||||
assert_eq!(parse_color("teal"), Some(Color::Rgb(0, 128, 128)));
|
||||
assert_eq!(parse_color("thistle"), Some(Color::Rgb(216, 191, 216)));
|
||||
assert_eq!(parse_color("tomato"), Some(Color::Rgb(255, 99, 71)));
|
||||
assert_eq!(parse_color("turquoise"), Some(Color::Rgb(64, 224, 208)));
|
||||
assert_eq!(parse_color("violet"), Some(Color::Rgb(238, 130, 238)));
|
||||
assert_eq!(parse_color("wheat"), Some(Color::Rgb(245, 222, 179)));
|
||||
assert_eq!(parse_color("whitesmoke"), Some(Color::Rgb(245, 245, 245)));
|
||||
assert_eq!(parse_color("yellowgreen"), Some(Color::Rgb(154, 205, 50)));
|
||||
// -- hex and rgb
|
||||
assert_eq!(
|
||||
parse_color("rgb(255, 64, 32)").unwrap(),
|
||||
Color::Rgb(255, 64, 32)
|
||||
);
|
||||
assert!(parse_color("redd").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Deps
|
||||
extern crate rand;
|
||||
// Ext
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
||||
|
||||
@@ -185,6 +185,13 @@ pub fn make_fsentry(path: PathBuf, is_dir: bool) -> FsEntry {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### create_file_ioers
|
||||
///
|
||||
/// Open a file with two handlers, the first is to read, the second is to write
|
||||
pub fn create_file_ioers(p: &Path) -> (File, File) {
|
||||
(File::open(p).ok().unwrap(), File::create(p).ok().unwrap())
|
||||
}
|
||||
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
@@ -245,4 +252,10 @@ mod test {
|
||||
assert!(make_dir_at(tmpdir.path(), "docs").is_ok());
|
||||
assert!(make_dir_at(PathBuf::from("/aaaaa/bbbbb/cccc").as_path(), "docs").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_test_helpers_create_file_ioers() {
|
||||
let (_, tmp) = create_sample_file_entry();
|
||||
let _ = create_file_ioers(tmp.path());
|
||||
}
|
||||
}
|
||||
|
||||
26
themes/default.toml
Normal file
26
themes/default.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
auth_address = "Yellow"
|
||||
auth_bookmarks = "LightGreen"
|
||||
auth_password = "LightBlue"
|
||||
auth_port = "LightCyan"
|
||||
auth_protocol = "LightGreen"
|
||||
auth_recents = "LightBlue"
|
||||
auth_username = "LightMagenta"
|
||||
misc_error_dialog = "Red"
|
||||
misc_input_dialog = "Default"
|
||||
misc_keys = "Cyan"
|
||||
misc_quit_dialog = "Yellow"
|
||||
misc_save_dialog = "LightCyan"
|
||||
misc_warn_dialog = "LightRed"
|
||||
transfer_local_explorer_background = "Default"
|
||||
transfer_local_explorer_foreground = "Default"
|
||||
transfer_local_explorer_highlighted = "Yellow"
|
||||
transfer_log_background = "Default"
|
||||
transfer_log_window = "LightGreen"
|
||||
transfer_progress_bar_full = "Green"
|
||||
transfer_progress_bar_partial = "Green"
|
||||
transfer_remote_explorer_background = "Default"
|
||||
transfer_remote_explorer_foreground = "Default"
|
||||
transfer_remote_explorer_highlighted = "LightBlue"
|
||||
transfer_status_hidden = "LightBlue"
|
||||
transfer_status_sorting = "LightYellow"
|
||||
transfer_status_sync_browsing = "LightGreen"
|
||||
26
themes/earth-wind-fire.toml
Normal file
26
themes/earth-wind-fire.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
auth_address = "Yellow"
|
||||
auth_bookmarks = "skyblue"
|
||||
auth_password = "#c43bff"
|
||||
auth_port = "lime"
|
||||
auth_protocol = "orangered"
|
||||
auth_recents = "deepskyblue"
|
||||
auth_username = "aqua"
|
||||
misc_error_dialog = "crimson"
|
||||
misc_input_dialog = "turquoise"
|
||||
misc_keys = "deeppink"
|
||||
misc_quit_dialog = "lime"
|
||||
misc_save_dialog = "gold"
|
||||
misc_warn_dialog = "orangered"
|
||||
transfer_local_explorer_background = "Default"
|
||||
transfer_local_explorer_foreground = "Default"
|
||||
transfer_local_explorer_highlighted = "aquamarine"
|
||||
transfer_log_background = "Default"
|
||||
transfer_log_window = "#c43bff"
|
||||
transfer_progress_bar_full = "deeppink"
|
||||
transfer_progress_bar_partial = "turquoise"
|
||||
transfer_remote_explorer_background = "Default"
|
||||
transfer_remote_explorer_foreground = "Default"
|
||||
transfer_remote_explorer_highlighted = "greenyellow"
|
||||
transfer_status_hidden = "lime"
|
||||
transfer_status_sorting = "orangered"
|
||||
transfer_status_sync_browsing = "darkturquoise"
|
||||
26
themes/horizon.toml
Normal file
26
themes/horizon.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
auth_address = "salmon"
|
||||
auth_bookmarks = "cornflowerblue"
|
||||
auth_password = "crimson"
|
||||
auth_port = "tomato"
|
||||
auth_protocol = "coral"
|
||||
auth_recents = "royalblue"
|
||||
auth_username = "orangered"
|
||||
misc_error_dialog = "crimson"
|
||||
misc_input_dialog = "gold"
|
||||
misc_keys = "deeppink"
|
||||
misc_quit_dialog = "coral"
|
||||
misc_save_dialog = "tomato"
|
||||
misc_warn_dialog = "orangered"
|
||||
transfer_local_explorer_background = "Default"
|
||||
transfer_local_explorer_foreground = "lightcoral"
|
||||
transfer_local_explorer_highlighted = "coral"
|
||||
transfer_log_background = "Default"
|
||||
transfer_log_window = "royalblue"
|
||||
transfer_progress_bar_full = "hotpink"
|
||||
transfer_progress_bar_partial = "deeppink"
|
||||
transfer_remote_explorer_background = "Default"
|
||||
transfer_remote_explorer_foreground = "lightsalmon"
|
||||
transfer_remote_explorer_highlighted = "salmon"
|
||||
transfer_status_hidden = "orange"
|
||||
transfer_status_sorting = "gold"
|
||||
transfer_status_sync_browsing = "tomato"
|
||||
26
themes/mono-bright.toml
Normal file
26
themes/mono-bright.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
auth_address = "black"
|
||||
auth_bookmarks = "#bbbbbb"
|
||||
auth_password = "black"
|
||||
auth_port = "black"
|
||||
auth_protocol = "black"
|
||||
auth_recents = "#bbbbbb"
|
||||
auth_username = "black"
|
||||
misc_error_dialog = "black"
|
||||
misc_input_dialog = "black"
|
||||
misc_keys = "black"
|
||||
misc_quit_dialog = "black"
|
||||
misc_save_dialog = "black"
|
||||
misc_warn_dialog = "black"
|
||||
transfer_local_explorer_background = "Default"
|
||||
transfer_local_explorer_foreground = "Default"
|
||||
transfer_local_explorer_highlighted = "#bbbbbb"
|
||||
transfer_log_background = "Default"
|
||||
transfer_log_window = "black"
|
||||
transfer_progress_bar_full = "black"
|
||||
transfer_progress_bar_partial = "black"
|
||||
transfer_remote_explorer_background = "Default"
|
||||
transfer_remote_explorer_foreground = "Default"
|
||||
transfer_remote_explorer_highlighted = "#bbbbbb"
|
||||
transfer_status_hidden = "black"
|
||||
transfer_status_sorting = "black"
|
||||
transfer_status_sync_browsing = "black"
|
||||
26
themes/mono-dark.toml
Normal file
26
themes/mono-dark.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
auth_address = "white"
|
||||
auth_bookmarks = "white"
|
||||
auth_password = "white"
|
||||
auth_port = "white"
|
||||
auth_protocol = "white"
|
||||
auth_recents = "white"
|
||||
auth_username = "white"
|
||||
misc_error_dialog = "white"
|
||||
misc_input_dialog = "white"
|
||||
misc_keys = "white"
|
||||
misc_quit_dialog = "white"
|
||||
misc_save_dialog = "white"
|
||||
misc_warn_dialog = "white"
|
||||
transfer_local_explorer_background = "Default"
|
||||
transfer_local_explorer_foreground = "Default"
|
||||
transfer_local_explorer_highlighted = "white"
|
||||
transfer_log_background = "Default"
|
||||
transfer_log_window = "white"
|
||||
transfer_progress_bar_full = "white"
|
||||
transfer_progress_bar_partial = "white"
|
||||
transfer_remote_explorer_background = "Default"
|
||||
transfer_remote_explorer_foreground = "Default"
|
||||
transfer_remote_explorer_highlighted = "white"
|
||||
transfer_status_hidden = "white"
|
||||
transfer_status_sorting = "white"
|
||||
transfer_status_sync_browsing = "white"
|
||||
26
themes/sugarplum.toml
Normal file
26
themes/sugarplum.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
auth_address = "hotpink"
|
||||
auth_bookmarks = "pink"
|
||||
auth_password = "violet"
|
||||
auth_port = "plum"
|
||||
auth_protocol = "deeppink"
|
||||
auth_recents = "lightpink"
|
||||
auth_username = "orchid"
|
||||
misc_error_dialog = "mediumvioletred"
|
||||
misc_input_dialog = "plum"
|
||||
misc_keys = "deeppink"
|
||||
misc_quit_dialog = "lightcoral"
|
||||
misc_save_dialog = "violet"
|
||||
misc_warn_dialog = "hotpink"
|
||||
transfer_local_explorer_background = "Default"
|
||||
transfer_local_explorer_foreground = "pink"
|
||||
transfer_local_explorer_highlighted = "hotpink"
|
||||
transfer_log_background = "Default"
|
||||
transfer_log_window = "palevioletred"
|
||||
transfer_progress_bar_full = "hotpink"
|
||||
transfer_progress_bar_partial = "deeppink"
|
||||
transfer_remote_explorer_background = "Default"
|
||||
transfer_remote_explorer_foreground = "plum"
|
||||
transfer_remote_explorer_highlighted = "violet"
|
||||
transfer_status_hidden = "violet"
|
||||
transfer_status_sorting = "plum"
|
||||
transfer_status_sync_browsing = "orchid"
|
||||
26
themes/ubuntu.toml
Normal file
26
themes/ubuntu.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
auth_address = "LightYellow"
|
||||
auth_bookmarks = "springgreen"
|
||||
auth_password = "deepskyblue"
|
||||
auth_port = "LightCyan"
|
||||
auth_protocol = "LightGreen"
|
||||
auth_recents = "aquamarine"
|
||||
auth_username = "hotpink"
|
||||
misc_error_dialog = "orangered"
|
||||
misc_input_dialog = "snow"
|
||||
misc_keys = "LightCyan"
|
||||
misc_quit_dialog = "LightYellow"
|
||||
misc_save_dialog = "LightCyan"
|
||||
misc_warn_dialog = "tomato"
|
||||
transfer_local_explorer_background = "Default"
|
||||
transfer_local_explorer_foreground = "Default"
|
||||
transfer_local_explorer_highlighted = "Yellow"
|
||||
transfer_log_background = "Default"
|
||||
transfer_log_window = "lawngreen"
|
||||
transfer_progress_bar_full = "lawngreen"
|
||||
transfer_progress_bar_partial = "lawngreen"
|
||||
transfer_remote_explorer_background = "Default"
|
||||
transfer_remote_explorer_foreground = "Default"
|
||||
transfer_remote_explorer_highlighted = "turquoise"
|
||||
transfer_status_hidden = "deepskyblue"
|
||||
transfer_status_sorting = "LightYellow"
|
||||
transfer_status_sync_browsing = "LightGreen"
|
||||
26
themes/veeso.toml
Normal file
26
themes/veeso.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
auth_address = "Yellow"
|
||||
auth_bookmarks = "plum"
|
||||
auth_password = "LightBlue"
|
||||
auth_port = "turquoise"
|
||||
auth_protocol = "greenyellow"
|
||||
auth_recents = "paleturquoise"
|
||||
auth_username = "deeppink"
|
||||
misc_error_dialog = "crimson"
|
||||
misc_input_dialog = "snow"
|
||||
misc_keys = "deeppink"
|
||||
misc_quit_dialog = "tomato"
|
||||
misc_save_dialog = "gold"
|
||||
misc_warn_dialog = "orangered"
|
||||
transfer_local_explorer_background = "Default"
|
||||
transfer_local_explorer_foreground = "Default"
|
||||
transfer_local_explorer_highlighted = "orange"
|
||||
transfer_log_background = "Default"
|
||||
transfer_log_window = "limegreen"
|
||||
transfer_progress_bar_full = "lawngreen"
|
||||
transfer_progress_bar_partial = "limegreen"
|
||||
transfer_remote_explorer_background = "Default"
|
||||
transfer_remote_explorer_foreground = "Default"
|
||||
transfer_remote_explorer_highlighted = "turquoise"
|
||||
transfer_status_hidden = "dodgerblue"
|
||||
transfer_status_sorting = "LightYellow"
|
||||
transfer_status_sync_browsing = "palegreen"
|
||||
Reference in New Issue
Block a user