mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Merge branch 'bookmarks' into 0.2.0
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,3 +13,6 @@
|
||||
**/*.rs.bk
|
||||
|
||||
# End of https://www.gitignore.io/api/rust
|
||||
|
||||
*.rpm
|
||||
*.deb
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -2,6 +2,7 @@
|
||||
|
||||
- [Changelog](#changelog)
|
||||
- [0.2.0](#020)
|
||||
- [0.1.3](#013)
|
||||
- [0.1.2](#012)
|
||||
- [0.1.1](#011)
|
||||
- [0.1.0](#010)
|
||||
@@ -12,6 +13,31 @@
|
||||
|
||||
Released on ??
|
||||
|
||||
- **Bookmarks**
|
||||
- Bookmarks and recent connections are now displayed in the home page
|
||||
- Bookmarks are saved at
|
||||
- Linux: `/home/alice/.config/termscp/bookmarks.toml`
|
||||
- Windows: `C:\Users\Alice\AppData\Roaming\termscp\bookmarks.toml`
|
||||
- MacOS: `/Users/Alice/Library/Application Support/termscp/bookmarks.toml`
|
||||
|
||||
## 0.1.3
|
||||
|
||||
Released on 14/12/2020
|
||||
|
||||
- Enhancements:
|
||||
- File transfer:
|
||||
- Read buffer is now 65536 bytes long
|
||||
- File explorer:
|
||||
- Fixed color mismatch in local explorer
|
||||
- Explorer tabs have now 70% of layout height, while logging area is 30%
|
||||
- Highlight selected entry in tabs, only when the tab is active
|
||||
- Auth page:
|
||||
- align popup text to center
|
||||
- Keybindings:
|
||||
- `L`: Refresh directory content
|
||||
- Bugfix:
|
||||
- Fixed memory vulnerability in Windows version
|
||||
|
||||
## 0.1.2
|
||||
|
||||
Released on 13/12/2020
|
||||
|
||||
121
Cargo.lock
generated
121
Cargo.lock
generated
@@ -9,18 +9,47 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"constant_time_eq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.4.0"
|
||||
@@ -79,6 +108,12 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.1"
|
||||
@@ -95,6 +130,17 @@ version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if 1.0.0",
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.18.2"
|
||||
@@ -120,6 +166,26 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -501,6 +567,17 @@ version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall",
|
||||
"rust-argon2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.4.2"
|
||||
@@ -538,6 +615,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-argon2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blake2b_simd",
|
||||
"constant_time_eq",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.19"
|
||||
@@ -577,6 +666,26 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.1.16"
|
||||
@@ -665,15 +774,18 @@ dependencies = [
|
||||
"bytesize",
|
||||
"chrono",
|
||||
"crossterm",
|
||||
"dirs",
|
||||
"ftp4",
|
||||
"getopts",
|
||||
"hostname",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"rpassword",
|
||||
"serde",
|
||||
"ssh2",
|
||||
"tempfile",
|
||||
"textwrap",
|
||||
"toml",
|
||||
"tui",
|
||||
"unicode-width",
|
||||
"users",
|
||||
@@ -710,6 +822,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui"
|
||||
version = "0.13.0"
|
||||
|
||||
23
Cargo.toml
23
Cargo.toml
@@ -15,20 +15,23 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bytesize = "1.0.1"
|
||||
chrono = "0.4.19"
|
||||
crossterm = "0.18.2"
|
||||
dirs = "3.0.1"
|
||||
ftp4 = { version = "^4.0.1", features = ["secure"] }
|
||||
getopts = "0.2.21"
|
||||
ssh2 = "0.9.0"
|
||||
tui = { version = "0.13.0", features = ["crossterm"], default-features = false }
|
||||
whoami = "1.0.0"
|
||||
rpassword = "5.0.0"
|
||||
unicode-width = "0.1.7"
|
||||
chrono = "0.4.19"
|
||||
bytesize = "1.0.1"
|
||||
textwrap = "0.13.0"
|
||||
regex = "1.4.2"
|
||||
lazy_static = "1.4.0"
|
||||
hostname = "0.3.1"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.4.2"
|
||||
rpassword = "5.0.0"
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
ssh2 = "0.9.0"
|
||||
textwrap = "0.13.0"
|
||||
toml = "0.5.7"
|
||||
tui = { version = "0.13.0", features = ["crossterm"], default-features = false }
|
||||
unicode-width = "0.1.7"
|
||||
whoami = "1.0.0"
|
||||
|
||||
[target.'cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))'.dependencies]
|
||||
users = "0.11.0"
|
||||
|
||||
38
README.md
38
README.md
@@ -1,12 +1,12 @@
|
||||
# TermSCP
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://github.com/ChristianVisintin/TermSCP) [](https://github.com/ChristianVisintin/TermSCP/issues) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://github.com/ChristianVisintin/TermSCP) [](https://github.com/ChristianVisintin/TermSCP/issues) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
|
||||
[](https://github.com/ChristianVisintin/TermSCP/actions) [](https://github.com/ChristianVisintin/TermSCP/actions) [](https://github.com/ChristianVisintin/TermSCP/actions)
|
||||
|
||||
~ Basically, WinSCP on a terminal ~
|
||||
Developed by Christian Visintin
|
||||
Current version: 0.1.2 (13/12/2020)
|
||||
Current version: 0.1.3 (13/12/2020)
|
||||
|
||||
---
|
||||
|
||||
@@ -21,8 +21,9 @@ Current version: 0.1.2 (13/12/2020)
|
||||
- [Chocolatey 🍫](#chocolatey-)
|
||||
- [Brew 🍻](#brew-)
|
||||
- [Usage ❓](#usage-)
|
||||
- [Address argument](#address-argument)
|
||||
- [How Password can be provided](#how-password-can-be-provided)
|
||||
- [Address argument 🌎](#address-argument-)
|
||||
- [How Password can be provided 🔐](#how-password-can-be-provided-)
|
||||
- [Bookmarks ⭐](#bookmarks-)
|
||||
- [Keybindings ⌨](#keybindings-)
|
||||
- [Documentation 📚](#documentation-)
|
||||
- [Known issues 🧻](#known-issues-)
|
||||
@@ -74,8 +75,8 @@ cargo install termscp
|
||||
|
||||
### Deb package 📦
|
||||
|
||||
Get `deb` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.2_amd64.deb)
|
||||
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.2_amd64.deb`
|
||||
Get `deb` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.3_amd64.deb)
|
||||
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.3_amd64.deb`
|
||||
|
||||
then install through dpkg:
|
||||
|
||||
@@ -87,8 +88,8 @@ gdebi termscp_*.deb
|
||||
|
||||
### RPM package 📦
|
||||
|
||||
Get `rpm` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.2-1.x86_64.rpm)
|
||||
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.2-1.x86_64.rpm`
|
||||
Get `rpm` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.3-1.x86_64.rpm)
|
||||
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.3-1.x86_64.rpm`
|
||||
|
||||
then install through rpm:
|
||||
|
||||
@@ -106,7 +107,7 @@ Start PowerShell as administrator and run
|
||||
choco install termscp
|
||||
```
|
||||
|
||||
Alternatively you can download the ZIP file from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp.0.1.2.nupkg)
|
||||
Alternatively you can download the ZIP file from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp.0.1.3.nupkg)
|
||||
|
||||
and then with PowerShell started with administrator previleges, run:
|
||||
|
||||
@@ -139,7 +140,7 @@ TermSCP can be started in two different mode, if no extra arguments is provided,
|
||||
|
||||
Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server.
|
||||
|
||||
### Address argument
|
||||
### Address argument 🌎
|
||||
|
||||
The address argument has the following syntax:
|
||||
|
||||
@@ -167,7 +168,7 @@ Let's see some example of this particular syntax, since it's very comfortable an
|
||||
termscp scp://omar@192.168.1.31:4022
|
||||
```
|
||||
|
||||
#### How Password can be provided
|
||||
#### How Password can be provided 🔐
|
||||
|
||||
You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
|
||||
Password can be basically provided through 3 ways when address argument is provided:
|
||||
@@ -178,6 +179,18 @@ Password can be basically provided through 3 ways when address argument is provi
|
||||
|
||||
---
|
||||
|
||||
## Bookmarks ⭐
|
||||
|
||||
Since TermSCP 0.2.0, it is possible to save favourites hosts, which can be then loaded quickly from the main layout of termscp.
|
||||
TermSCP will also save the last 16 hosts you connected to.
|
||||
This feature allows you to load all the parameters required to connect to a certain remote, simply selecting the bookmark in the tab under the authentication form.
|
||||
For safety reason, termscp **WILL NEVER** save your passwords; you'll need to provide it everytime (Don't worry, every time you select a bookmark, the cursor will be placed in the password form to speed up this process 😉).
|
||||
|
||||
To create a bookmark, just fulfill the authentication form and then input `CTRL+S`; you'll then be asked to give a name to your bookmark, and tadah, the bookmark has been created.
|
||||
If you go to [galler](#gallery-), there is a GIF showing how bookmarks work 💪.
|
||||
|
||||
---
|
||||
|
||||
## Keybindings ⌨
|
||||
|
||||
| Key | Command |
|
||||
@@ -198,11 +211,12 @@ Password can be basically provided through 3 ways when address argument is provi
|
||||
| `<G>` | Go to supplied path |
|
||||
| `<H>` | Show help |
|
||||
| `<I>` | Show info about selected file or directory |
|
||||
| `<L>` | Reload current directory's content |
|
||||
| `<Q>` | Quit TermSCP |
|
||||
| `<R>` | Rename file |
|
||||
| `<U>` | Go to parent directory |
|
||||
| `<DEL>` | Delete file |
|
||||
| `CTRL+C` | Abort file transfer process |
|
||||
| `<CTRL+C>` | Abort file transfer process |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 632 KiB After Width: | Height: | Size: 635 KiB |
20
dist/build/deploy.sh
vendored
Executable file
20
dist/build/deploy.sh
vendored
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: deploy.sh <version>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$1
|
||||
|
||||
# Build x86_64
|
||||
cd x86_64/
|
||||
docker build --tag termscp-${VERSION}-x86_64 .
|
||||
# Get pkgs
|
||||
cd -
|
||||
# Create container
|
||||
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64 termscp-${VERSION}-x86_64)
|
||||
docker cp ${CONTAINER_NAME}:/usr/src/TermSCP/target/debian/termscp_${VERSION}_amd64.deb .
|
||||
docker cp ${CONTAINER_NAME}:/usr/src/TermSCP/target/release/rpmbuild/RPMS/x86_64/termscp-${VERSION}-1.x86_64.rpm .
|
||||
|
||||
exit $?
|
||||
19
dist/build/x86_64/Dockerfile
vendored
Normal file
19
dist/build/x86_64/Dockerfile
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM rust:1.48.0 AS builder
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Add toolchains
|
||||
RUN rustup target add x86_64-unknown-linux-gnu
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y rpm
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/ChristianVisintin/TermSCP.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/TermSCP/
|
||||
# Install cargo RPM/Deb
|
||||
RUN cargo install cargo-deb cargo-rpm
|
||||
# Build for x86_64
|
||||
RUN cargo build --release --target x86_64-unknown-linux-gnu
|
||||
# Build pkgs
|
||||
RUN cargo deb && cargo rpm init && cargo rpm build
|
||||
|
||||
CMD ["sh"]
|
||||
112
src/bookmarks/mod.rs
Normal file
112
src/bookmarks/mod.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! ## Bookmarks
|
||||
//!
|
||||
//! `bookmarks` is the module which provides data types and de/serializer for bookmarks
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
pub mod serializer;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## UserHosts
|
||||
///
|
||||
/// UserHosts contains all the hosts saved by the user in the data storage
|
||||
/// It contains both `Bookmark`
|
||||
pub struct UserHosts {
|
||||
pub bookmarks: HashMap<String, Bookmark>,
|
||||
pub recents: HashMap<String, Bookmark>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug, PartialEq)]
|
||||
/// ## Bookmark
|
||||
///
|
||||
/// Bookmark describes a single bookmark entry in the user hosts storage
|
||||
pub struct Bookmark {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub protocol: String,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
// 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(std::fmt::Debug)]
|
||||
pub enum SerializerErrorKind {
|
||||
IoError,
|
||||
SerializationError,
|
||||
SyntaxError,
|
||||
}
|
||||
|
||||
impl Default for UserHosts {
|
||||
fn default() -> Self {
|
||||
UserHosts {
|
||||
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 {
|
||||
let err: String = match &self.kind {
|
||||
SerializerErrorKind::IoError => String::from("IO Error"),
|
||||
SerializerErrorKind::SerializationError => String::from("Serialization error"),
|
||||
SerializerErrorKind::SyntaxError => String::from("Syntax error"),
|
||||
};
|
||||
match &self.msg {
|
||||
Some(msg) => write!(f, "{} ({})", err, msg),
|
||||
None => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
213
src/bookmarks/serializer.rs
Normal file
213
src/bookmarks/serializer.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
//! ## Serializer
|
||||
//!
|
||||
//! `serializer` is the module which provides the serializer/deserializer for bookmarks
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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(),
|
||||
))
|
||||
}
|
||||
};
|
||||
// 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(),
|
||||
));
|
||||
}
|
||||
// Deserialize
|
||||
match toml::de::from_str(data.as_str()) {
|
||||
Ok(hosts) => Ok(hosts),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::super::Bookmark;
|
||||
use super::*;
|
||||
|
||||
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"));
|
||||
// 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"));
|
||||
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"));
|
||||
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"));
|
||||
}
|
||||
|
||||
#[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"),
|
||||
},
|
||||
);
|
||||
bookmarks.insert(
|
||||
String::from("msi-estrem"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.30"),
|
||||
port: 4022,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("cvisintin"),
|
||||
},
|
||||
);
|
||||
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"),
|
||||
},
|
||||
);
|
||||
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" }
|
||||
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin" }
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -324,7 +324,7 @@ impl Localhost {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
||||
let attr: Metadata = match fs::metadata(path.clone()) {
|
||||
let attr: Metadata = match fs::metadata(path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => return Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
|
||||
};
|
||||
@@ -418,7 +418,7 @@ impl Localhost {
|
||||
/// ### file_exists
|
||||
///
|
||||
/// Returns whether provided file path exists
|
||||
fn file_exists(&self, path: &Path) -> bool {
|
||||
pub fn file_exists(&self, path: &Path) -> bool {
|
||||
path.exists()
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#[macro_use] extern crate lazy_static;
|
||||
|
||||
pub mod activity_manager;
|
||||
pub mod bookmarks;
|
||||
pub mod filetransfer;
|
||||
pub mod fs;
|
||||
pub mod host;
|
||||
|
||||
@@ -36,6 +36,7 @@ use std::time::Duration;
|
||||
|
||||
// Include
|
||||
mod activity_manager;
|
||||
mod bookmarks;
|
||||
mod filetransfer;
|
||||
mod fs;
|
||||
mod host;
|
||||
|
||||
@@ -1,542 +0,0 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Dependencies
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
extern crate unicode_width;
|
||||
|
||||
// locals
|
||||
use super::{Activity, Context};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
|
||||
// Includes
|
||||
use crossterm::event::Event as InputEvent;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use tui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, Clear, Paragraph, Tabs},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// ### InputField
|
||||
///
|
||||
/// InputField describes the current input field to edit
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
enum InputField {
|
||||
Address,
|
||||
Port,
|
||||
Protocol,
|
||||
Username,
|
||||
Password,
|
||||
}
|
||||
|
||||
/// ### InputMode
|
||||
///
|
||||
/// InputMode describes the current input mode
|
||||
/// Each input mode handle the input events in a different way
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
enum InputMode {
|
||||
Text,
|
||||
Popup,
|
||||
}
|
||||
|
||||
/// ### AuthActivity
|
||||
///
|
||||
/// AuthActivity is the data holder for the authentication activity
|
||||
pub struct AuthActivity {
|
||||
pub address: String,
|
||||
pub port: String,
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub submit: bool, // becomes true after user has submitted fields
|
||||
pub quit: bool, // Becomes true if user has pressed esc
|
||||
context: Option<Context>,
|
||||
selected_field: InputField,
|
||||
input_mode: InputMode,
|
||||
popup_message: Option<String>,
|
||||
password_placeholder: String,
|
||||
redraw: bool, // Should ui actually be redrawned?
|
||||
}
|
||||
|
||||
impl Default for AuthActivity {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new AuthActivity
|
||||
pub fn new() -> AuthActivity {
|
||||
AuthActivity {
|
||||
address: String::new(),
|
||||
port: String::from("22"),
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
submit: false,
|
||||
quit: false,
|
||||
context: None,
|
||||
selected_field: InputField::Address,
|
||||
input_mode: InputMode::Text,
|
||||
popup_message: None,
|
||||
password_placeholder: String::new(),
|
||||
redraw: true, // True at startup
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_input_mode
|
||||
///
|
||||
/// Update input mode based on current parameters
|
||||
fn select_input_mode(&mut self) -> InputMode {
|
||||
if self.popup_message.is_some() {
|
||||
return InputMode::Popup;
|
||||
}
|
||||
// Default to text
|
||||
InputMode::Text
|
||||
}
|
||||
|
||||
/// ### handle_input_event
|
||||
///
|
||||
/// Handle input event, based on current input mode
|
||||
fn handle_input_event(&mut self, ev: &InputEvent) {
|
||||
match self.input_mode {
|
||||
InputMode::Text => self.handle_input_event_mode_text(ev),
|
||||
InputMode::Popup => self.handle_input_event_mode_popup(ev),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_text
|
||||
///
|
||||
/// Handler for input event when in textmode
|
||||
fn handle_input_event_mode_text(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
self.quit = true;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Handle submit
|
||||
// Check form
|
||||
// Check address
|
||||
if self.address.is_empty() {
|
||||
self.popup_message = Some(String::from("Invalid address"));
|
||||
return;
|
||||
}
|
||||
// Check port
|
||||
// Convert port to number
|
||||
match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.popup_message =
|
||||
Some(String::from("Specified port must be in range 0-65535"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
self.popup_message =
|
||||
Some(String::from("Specified port is not a number"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check username
|
||||
//if self.username.len() == 0 {
|
||||
// self.popup_message = Some(String::from("Invalid username"));
|
||||
// return;
|
||||
//}
|
||||
// Everything OK, set enter
|
||||
self.submit = true;
|
||||
self.popup_message =
|
||||
Some(format!("Connecting to {}:{}...", self.address, self.port));
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
// Pop last char
|
||||
match self.selected_field {
|
||||
InputField::Address => {
|
||||
let _ = self.address.pop();
|
||||
}
|
||||
InputField::Password => {
|
||||
let _ = self.password.pop();
|
||||
}
|
||||
InputField::Username => {
|
||||
let _ = self.username.pop();
|
||||
}
|
||||
InputField::Port => {
|
||||
let _ = self.port.pop();
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
};
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Move item up
|
||||
self.selected_field = match self.selected_field {
|
||||
InputField::Address => InputField::Password, // End of list (wrap)
|
||||
InputField::Port => InputField::Address,
|
||||
InputField::Protocol => InputField::Port,
|
||||
InputField::Username => InputField::Protocol,
|
||||
InputField::Password => InputField::Username,
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Tab => {
|
||||
// Move item down
|
||||
self.selected_field = match self.selected_field {
|
||||
InputField::Address => InputField::Port,
|
||||
InputField::Port => InputField::Protocol,
|
||||
InputField::Protocol => InputField::Username,
|
||||
InputField::Username => InputField::Password,
|
||||
InputField::Password => InputField::Address, // End of list (wrap)
|
||||
}
|
||||
}
|
||||
KeyCode::Char(ch) => {
|
||||
match self.selected_field {
|
||||
InputField::Address => self.address.push(ch),
|
||||
InputField::Password => self.password.push(ch),
|
||||
InputField::Username => self.username.push(ch),
|
||||
InputField::Port => {
|
||||
// Value must be numeric
|
||||
if ch.is_numeric() {
|
||||
self.port.push(ch);
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
// If current field is Protocol handle event... (move element left)
|
||||
if self.selected_field == InputField::Protocol {
|
||||
self.protocol = match self.protocol {
|
||||
FileTransferProtocol::Sftp => FileTransferProtocol::Ftp(true), // End of list (wrap)
|
||||
FileTransferProtocol::Scp => FileTransferProtocol::Sftp,
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => FileTransferProtocol::Scp,
|
||||
true => FileTransferProtocol::Ftp(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
// If current field is Protocol handle event... ( move element right )
|
||||
if self.selected_field == InputField::Protocol {
|
||||
self.protocol = match self.protocol {
|
||||
FileTransferProtocol::Sftp => FileTransferProtocol::Scp,
|
||||
FileTransferProtocol::Scp => FileTransferProtocol::Ftp(false),
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => FileTransferProtocol::Ftp(true),
|
||||
true => FileTransferProtocol::Sftp, // End of list (wrap)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_text
|
||||
///
|
||||
/// Handler for input event when in popup mode
|
||||
fn handle_input_event_mode_popup(&mut self, ev: &InputEvent) {
|
||||
// Only enter should be allowed here
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let KeyCode::Enter = key.code {
|
||||
self.popup_message = None; // Hide popup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### draw_remote_address
|
||||
///
|
||||
/// Draw remote address block
|
||||
fn draw_remote_address(&self) -> Paragraph {
|
||||
Paragraph::new(self.address.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Address => Style::default().fg(Color::Yellow),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Remote address"),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_remote_port
|
||||
///
|
||||
/// Draw remote port block
|
||||
fn draw_remote_port(&self) -> Paragraph {
|
||||
Paragraph::new(self.port.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Port => Style::default().fg(Color::Cyan),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Remote port"))
|
||||
}
|
||||
|
||||
/// ### draw_protocol_select
|
||||
///
|
||||
/// Draw protocol select
|
||||
fn draw_protocol_select(&self) -> Tabs {
|
||||
let protocols: Vec<Spans> = vec![
|
||||
Spans::from("SFTP"),
|
||||
Spans::from("SCP"),
|
||||
Spans::from("FTP"),
|
||||
Spans::from("FTPS"),
|
||||
];
|
||||
let index: usize = match self.protocol {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => 2,
|
||||
true => 3,
|
||||
},
|
||||
};
|
||||
Tabs::new(protocols)
|
||||
.block(Block::default().borders(Borders::ALL).title("Protocol"))
|
||||
.select(index)
|
||||
.style(match self.selected_field {
|
||||
InputField::Protocol => Style::default().fg(Color::Green),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Green)
|
||||
.fg(Color::Black),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_protocol_username
|
||||
///
|
||||
/// Draw username block
|
||||
fn draw_protocol_username(&self) -> Paragraph {
|
||||
Paragraph::new(self.username.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Username => Style::default().fg(Color::Magenta),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Username"))
|
||||
}
|
||||
|
||||
/// ### draw_protocol_password
|
||||
///
|
||||
/// Draw password block
|
||||
fn draw_protocol_password(&mut self) -> Paragraph {
|
||||
// Create password secret
|
||||
self.password_placeholder = (0..self.password.width()).map(|_| "*").collect::<String>();
|
||||
Paragraph::new(self.password_placeholder.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Password => Style::default().fg(Color::LightBlue),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Password"))
|
||||
}
|
||||
|
||||
/// ### draw_header
|
||||
///
|
||||
/// Draw header
|
||||
fn draw_header(&self) -> Paragraph {
|
||||
Paragraph::new(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")
|
||||
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD))
|
||||
}
|
||||
|
||||
/// ### draw_footer
|
||||
///
|
||||
/// Draw authentication page footer
|
||||
fn draw_footer(&self) -> Paragraph {
|
||||
// Write header
|
||||
let (footer, h_style) = (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("<ESC>", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to exit, "),
|
||||
Span::styled("<UP,DOWN>", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to change input field, "),
|
||||
Span::styled("<ENTER>", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to submit form"),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
);
|
||||
let mut footer_text = Text::from(Spans::from(footer));
|
||||
footer_text.patch_style(h_style);
|
||||
Paragraph::new(footer_text)
|
||||
}
|
||||
|
||||
/// ### draw_popup
|
||||
///
|
||||
/// Draw popup block
|
||||
fn draw_popup(&self, r: Rect) -> (Paragraph, Rect) {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((80) / 2),
|
||||
Constraint::Percentage(20),
|
||||
Constraint::Percentage((80) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(r);
|
||||
let area: Rect = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((80) / 2),
|
||||
Constraint::Percentage(20),
|
||||
Constraint::Percentage((80) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_layout[1])[1];
|
||||
let popup: Paragraph = Paragraph::new(self.popup_message.as_ref().unwrap().as_ref())
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.block(Block::default().borders(Borders::ALL).title("Alert"));
|
||||
(popup, area)
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity for AuthActivity {
|
||||
/// ### on_create
|
||||
///
|
||||
/// `on_create` is the function which must be called to initialize the activity.
|
||||
/// `on_create` must initialize all the data structures used by the activity
|
||||
/// Context is taken from activity manager and will be released only when activity is destroyed
|
||||
fn on_create(&mut self, context: Context) {
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
let _ = self.context.as_mut().unwrap().terminal.clear();
|
||||
// Put raw mode on enabled
|
||||
let _ = enable_raw_mode();
|
||||
}
|
||||
|
||||
/// ### on_draw
|
||||
///
|
||||
/// `on_draw` is the function which draws the graphical interface.
|
||||
/// This function must be called at each tick to refresh the interface
|
||||
fn on_draw(&mut self) {
|
||||
// Context must be something
|
||||
if self.context.is_none() {
|
||||
return;
|
||||
}
|
||||
// Start catching Input Events
|
||||
if let Ok(input_events) = self.context.as_ref().unwrap().input_hnd.fetch_events() {
|
||||
if !input_events.is_empty() {
|
||||
self.redraw = true; // Set redraw to true if there is at least one event
|
||||
}
|
||||
// Iterate over input events
|
||||
for event in input_events.iter() {
|
||||
self.handle_input_event(event);
|
||||
}
|
||||
}
|
||||
// Redraw if necessary
|
||||
if self.redraw {
|
||||
// Determine input mode
|
||||
self.input_mode = self.select_input_mode();
|
||||
// draw interface
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// Draw header
|
||||
f.render_widget(self.draw_header(), chunks[0]);
|
||||
// Draw input fields
|
||||
f.render_widget(self.draw_remote_address(), chunks[1]);
|
||||
f.render_widget(self.draw_remote_port(), chunks[2]);
|
||||
f.render_widget(self.draw_protocol_select(), chunks[3]);
|
||||
f.render_widget(self.draw_protocol_username(), chunks[4]);
|
||||
f.render_widget(self.draw_protocol_password(), chunks[5]);
|
||||
// Draw footer
|
||||
f.render_widget(self.draw_footer(), chunks[6]);
|
||||
if self.popup_message.is_some() {
|
||||
let (popup, popup_area): (Paragraph, Rect) = self.draw_popup(f.size());
|
||||
f.render_widget(Clear, popup_area); //this clears out the background
|
||||
f.render_widget(popup, popup_area);
|
||||
}
|
||||
// Set cursor
|
||||
match self.selected_field {
|
||||
InputField::Address => f.set_cursor(
|
||||
chunks[1].x + self.address.width() as u16 + 1,
|
||||
chunks[1].y + 1,
|
||||
),
|
||||
InputField::Port => {
|
||||
f.set_cursor(chunks[2].x + self.port.width() as u16 + 1, chunks[2].y + 1)
|
||||
}
|
||||
InputField::Username => f.set_cursor(
|
||||
chunks[4].x + self.username.width() as u16 + 1,
|
||||
chunks[4].y + 1,
|
||||
),
|
||||
InputField::Password => f.set_cursor(
|
||||
chunks[5].x + self.password_placeholder.width() as u16 + 1,
|
||||
chunks[5].y + 1,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
// Reset ctx
|
||||
self.context = Some(ctx);
|
||||
// Set redraw to false
|
||||
self.redraw = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_destroy
|
||||
///
|
||||
/// `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.
|
||||
/// This function finally releases the context
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Disable raw mode
|
||||
let _ = disable_raw_mode();
|
||||
self.context.as_ref()?;
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
Some(mut ctx) => {
|
||||
let _ = ctx.terminal.clear();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
400
src/ui/activities/auth_activity/bookmarks.rs
Normal file
400
src/ui/activities/auth_activity/bookmarks.rs
Normal file
@@ -0,0 +1,400 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Dependencies
|
||||
extern crate dirs;
|
||||
|
||||
// Locals
|
||||
use super::{AuthActivity, Color, FileTransferProtocol, InputMode, PopupType, UserHosts};
|
||||
use crate::bookmarks::serializer::BookmarkSerializer;
|
||||
use crate::bookmarks::Bookmark;
|
||||
use crate::utils::time_to_str;
|
||||
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### read_bookmarks
|
||||
///
|
||||
/// Read bookmarks from data file; Show popup if necessary
|
||||
pub(super) fn read_bookmarks(&mut self) {
|
||||
// Init bookmarks
|
||||
if let Some(bookmark_file) = self.init_bookmarks() {
|
||||
// Read
|
||||
if self.context.is_some() {
|
||||
match self
|
||||
.context
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.local
|
||||
.open_file_read(bookmark_file.as_path())
|
||||
{
|
||||
Ok(reader) => {
|
||||
// Read bookmarks
|
||||
let deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
match deserializer.deserialize(Box::new(reader)) {
|
||||
Ok(bookmarks) => self.bookmarks = Some(bookmarks),
|
||||
Err(err) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Yellow,
|
||||
format!(
|
||||
"Could not read bookmarks from \"{}\": {}",
|
||||
bookmark_file.display(),
|
||||
err
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Yellow,
|
||||
format!(
|
||||
"Could not read bookmarks from \"{}\": {}",
|
||||
bookmark_file.display(),
|
||||
err
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### del_bookmark
|
||||
///
|
||||
/// Delete bookmark
|
||||
pub(super) fn del_bookmark(&mut self, idx: usize) {
|
||||
if let Some(hosts) = self.bookmarks.as_mut() {
|
||||
// Iterate over kyes
|
||||
let mut name: Option<String> = None;
|
||||
for (i, key) in hosts.bookmarks.keys().enumerate() {
|
||||
if i == idx {
|
||||
name = Some(key.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(name) = name {
|
||||
hosts.bookmarks.remove(name.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### load_bookmark
|
||||
///
|
||||
/// Load selected bookmark (at index) to input fields
|
||||
pub(super) fn load_bookmark(&mut self, idx: usize) {
|
||||
if let Some(hosts) = self.bookmarks.as_mut() {
|
||||
// Iterate over bookmarks
|
||||
for (i, bookmark) in hosts.bookmarks.values().enumerate() {
|
||||
if i == idx {
|
||||
// Load parameters
|
||||
self.address = bookmark.address.clone();
|
||||
self.port = bookmark.port.to_string();
|
||||
self.protocol = match bookmark.protocol.as_str().to_uppercase().as_str() {
|
||||
"FTP" => FileTransferProtocol::Ftp(false),
|
||||
"FTPS" => FileTransferProtocol::Ftp(true),
|
||||
"SCP" => FileTransferProtocol::Scp,
|
||||
_ => FileTransferProtocol::Sftp, // Default to SFTP
|
||||
};
|
||||
self.username = bookmark.username.clone();
|
||||
// Break
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### save_bookmark
|
||||
///
|
||||
/// Save current input fields as a bookmark
|
||||
pub(super) fn save_bookmark(&mut self, name: String) {
|
||||
if let Ok(host) = self.make_user_host() {
|
||||
if let Some(hosts) = self.bookmarks.as_mut() {
|
||||
hosts.bookmarks.insert(name, host);
|
||||
// Write bookmarks
|
||||
self.write_bookmarks();
|
||||
}
|
||||
}
|
||||
}
|
||||
/// ### del_recent
|
||||
///
|
||||
/// Delete recent
|
||||
pub(super) fn del_recent(&mut self, idx: usize) {
|
||||
if let Some(hosts) = self.bookmarks.as_mut() {
|
||||
// Iterate over kyes
|
||||
let mut name: Option<String> = None;
|
||||
for (i, key) in hosts.recents.keys().enumerate() {
|
||||
if i == idx {
|
||||
name = Some(key.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(name) = name {
|
||||
hosts.recents.remove(name.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### load_recent
|
||||
///
|
||||
/// Load selected recent (at index) to input fields
|
||||
pub(super) fn load_recent(&mut self, idx: usize) {
|
||||
if let Some(hosts) = self.bookmarks.as_mut() {
|
||||
// Iterate over bookmarks
|
||||
for (i, bookmark) in hosts.recents.values().enumerate() {
|
||||
if i == idx {
|
||||
// Load parameters
|
||||
self.address = bookmark.address.clone();
|
||||
self.port = bookmark.port.to_string();
|
||||
self.protocol = match bookmark.protocol.as_str().to_uppercase().as_str() {
|
||||
"FTP" => FileTransferProtocol::Ftp(false),
|
||||
"FTPS" => FileTransferProtocol::Ftp(true),
|
||||
"SCP" => FileTransferProtocol::Scp,
|
||||
_ => FileTransferProtocol::Sftp, // Default to SFTP
|
||||
};
|
||||
self.username = bookmark.username.clone();
|
||||
// Break
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### save_recent
|
||||
///
|
||||
/// Save current input fields as a "recent"
|
||||
pub(super) fn save_recent(&mut self) {
|
||||
if let Ok(host) = self.make_user_host() {
|
||||
if let Some(hosts) = self.bookmarks.as_mut() {
|
||||
// Check if duplicated
|
||||
for recent_host in hosts.recents.values() {
|
||||
if *recent_host == host {
|
||||
// Don't save duplicates
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If hosts size is bigger than 16; pop last
|
||||
if hosts.recents.len() >= 16 {
|
||||
let mut keys: Vec<String> = Vec::with_capacity(hosts.recents.len());
|
||||
for key in hosts.recents.keys() {
|
||||
keys.push(key.clone());
|
||||
}
|
||||
// Sort keys; NOTE: most recent is the last element
|
||||
keys.sort();
|
||||
// Delete keys starting from the last one
|
||||
for key in keys.iter() {
|
||||
let _ = hosts.recents.remove(key);
|
||||
// If length is < 16; break
|
||||
if hosts.recents.len() < 16 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create name
|
||||
let name: String = time_to_str(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
||||
// Save host to recents
|
||||
hosts.recents.insert(name, host);
|
||||
// Write bookmarks
|
||||
self.write_bookmarks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### make_user_host
|
||||
///
|
||||
/// Make user host from current input fields
|
||||
fn make_user_host(&mut self) -> Result<Bookmark, ()> {
|
||||
// Check port
|
||||
let port: u16 = match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
return Err(());
|
||||
}
|
||||
val as u16
|
||||
}
|
||||
Err(_) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
Ok(Bookmark {
|
||||
address: self.address.clone(),
|
||||
port,
|
||||
protocol: match self.protocol {
|
||||
FileTransferProtocol::Ftp(secure) => match secure {
|
||||
true => String::from("FTPS"),
|
||||
false => String::from("FTP"),
|
||||
},
|
||||
FileTransferProtocol::Scp => String::from("SCP"),
|
||||
FileTransferProtocol::Sftp => String::from("SFTP"),
|
||||
},
|
||||
username: self.username.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// ### write_bookmarks
|
||||
///
|
||||
/// Write bookmarks to file
|
||||
fn write_bookmarks(&mut self) {
|
||||
if self.bookmarks.is_some() && self.context.is_some() {
|
||||
// Open file for write
|
||||
if let Some(bookmarks_file) = self.init_bookmarks() {
|
||||
match self
|
||||
.context
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.local
|
||||
.open_file_write(bookmarks_file.as_path())
|
||||
{
|
||||
Ok(writer) => {
|
||||
let serializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
if let Err(err) = serializer
|
||||
.serialize(Box::new(writer), &self.bookmarks.as_ref().unwrap())
|
||||
{
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Yellow,
|
||||
format!(
|
||||
"Could not write default bookmarks at \"{}\": {}",
|
||||
bookmarks_file.display(),
|
||||
err
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Yellow,
|
||||
format!(
|
||||
"Could not write default bookmarks at \"{}\": {}",
|
||||
bookmarks_file.display(),
|
||||
err
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### init_bookmarks
|
||||
///
|
||||
/// Initialize bookmarks directory
|
||||
/// Returns bookmark path
|
||||
fn init_bookmarks(&mut self) -> Option<PathBuf> {
|
||||
// Get file
|
||||
lazy_static! {
|
||||
static ref CONF_DIR: Option<PathBuf> = dirs::config_dir();
|
||||
}
|
||||
if CONF_DIR.is_some() {
|
||||
// Get path of bookmarks
|
||||
let mut p: PathBuf = CONF_DIR.as_ref().unwrap().clone();
|
||||
// Append termscp dir
|
||||
p.push("termscp/");
|
||||
// Mkdir if doesn't exist
|
||||
if self.context.is_some() {
|
||||
if let Err(err) = self
|
||||
.context
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.local
|
||||
.mkdir_ex(p.as_path(), true)
|
||||
{
|
||||
// Show popup
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Yellow,
|
||||
format!(
|
||||
"Could not create configuration directory at \"{}\": {}",
|
||||
p.display(),
|
||||
err
|
||||
),
|
||||
));
|
||||
// Return None
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// Append bookmarks.toml
|
||||
p.push("bookmarks.toml");
|
||||
// If bookmarks.toml doesn't exist, initializae it
|
||||
if self.context.is_some()
|
||||
&& !self
|
||||
.context
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.local
|
||||
.file_exists(p.as_path())
|
||||
{
|
||||
// Write file
|
||||
let default_hosts: UserHosts = Default::default();
|
||||
match self
|
||||
.context
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.local
|
||||
.open_file_write(p.as_path())
|
||||
{
|
||||
Ok(writer) => {
|
||||
let serializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
// Serialize and write
|
||||
if let Err(err) = serializer.serialize(Box::new(writer), &default_hosts) {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Yellow,
|
||||
format!(
|
||||
"Could not write default bookmarks at \"{}\": {}",
|
||||
p.display(),
|
||||
err
|
||||
),
|
||||
));
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Yellow,
|
||||
format!(
|
||||
"Could not write default bookmarks at \"{}\": {}",
|
||||
p.display(),
|
||||
err
|
||||
),
|
||||
));
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// return path
|
||||
Some(p)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/ui/activities/auth_activity/callbacks.rs
Normal file
58
src/ui/activities/auth_activity/callbacks.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{AuthActivity, InputForm};
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### callback_nothing_to_do
|
||||
///
|
||||
/// Self titled
|
||||
pub(super) fn callback_nothing_to_do(&mut self) {}
|
||||
|
||||
/// ### callback_quit
|
||||
///
|
||||
/// Self titled
|
||||
pub(super) fn callback_quit(&mut self) {
|
||||
self.quit = true;
|
||||
}
|
||||
|
||||
/// ### callback_del_bookmark
|
||||
///
|
||||
/// Callback which deletes recent or bookmark based on current form
|
||||
pub(super) fn callback_del_bookmark(&mut self) {
|
||||
match self.input_form {
|
||||
InputForm::Bookmarks => self.del_bookmark(self.bookmarks_idx),
|
||||
InputForm::Recents => self.del_recent(self.recents_idx),
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_save_bookmark
|
||||
///
|
||||
/// Callback used to save bookmark with name
|
||||
pub(super) fn callback_save_bookmark(&mut self, input: String) {
|
||||
self.save_bookmark(input);
|
||||
}
|
||||
}
|
||||
487
src/ui/activities/auth_activity/input.rs
Normal file
487
src/ui/activities/auth_activity/input.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{
|
||||
AuthActivity, DialogCallback, DialogYesNoOption, FileTransferProtocol, InputEvent, InputField,
|
||||
InputForm, InputMode, OnInputSubmitCallback, PopupType,
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use tui::style::Color;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### handle_input_event
|
||||
///
|
||||
/// Handle input event, based on current input mode
|
||||
pub(super) fn handle_input_event(&mut self, ev: &InputEvent) {
|
||||
let popup: Option<PopupType> = match &self.input_mode {
|
||||
InputMode::Popup(ptype) => Some(ptype.clone()),
|
||||
_ => None,
|
||||
};
|
||||
match self.input_mode {
|
||||
InputMode::Form => self.handle_input_event_mode_form(ev),
|
||||
InputMode::Popup(_) => {
|
||||
if let Some(ptype) = popup {
|
||||
self.handle_input_event_mode_popup(ev, ptype)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form
|
||||
///
|
||||
/// Handler for input event when in form mode
|
||||
pub(super) fn handle_input_event_mode_form(&mut self, ev: &InputEvent) {
|
||||
match self.input_form {
|
||||
InputForm::AuthCredentials => self.handle_input_event_mode_form_auth(ev),
|
||||
InputForm::Bookmarks => self.handle_input_event_mode_form_bookmarks(ev),
|
||||
InputForm::Recents => self.handle_input_event_mode_form_recents(ev),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form_auth
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Auth
|
||||
pub(super) fn handle_input_event_mode_form_auth(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Show quit dialog
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Tab => self.input_form = InputForm::Bookmarks, // Move to bookmarks
|
||||
KeyCode::Enter => {
|
||||
// Handle submit
|
||||
// Check form
|
||||
// Check address
|
||||
if self.address.is_empty() {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
String::from("Invalid address"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
// Check port
|
||||
// Convert port to number
|
||||
match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Save recent
|
||||
self.save_recent();
|
||||
// Everything OK, set enter
|
||||
self.submit = true;
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
// Pop last char
|
||||
match self.selected_field {
|
||||
InputField::Address => {
|
||||
let _ = self.address.pop();
|
||||
}
|
||||
InputField::Password => {
|
||||
let _ = self.password.pop();
|
||||
}
|
||||
InputField::Username => {
|
||||
let _ = self.username.pop();
|
||||
}
|
||||
InputField::Port => {
|
||||
let _ = self.port.pop();
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
};
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Move item up
|
||||
self.selected_field = match self.selected_field {
|
||||
InputField::Address => InputField::Password, // End of list (wrap)
|
||||
InputField::Port => InputField::Address,
|
||||
InputField::Protocol => InputField::Port,
|
||||
InputField::Username => InputField::Protocol,
|
||||
InputField::Password => InputField::Username,
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
// Move item down
|
||||
self.selected_field = match self.selected_field {
|
||||
InputField::Address => InputField::Port,
|
||||
InputField::Port => InputField::Protocol,
|
||||
InputField::Protocol => InputField::Username,
|
||||
InputField::Username => InputField::Password,
|
||||
InputField::Password => InputField::Address, // End of list (wrap)
|
||||
}
|
||||
}
|
||||
KeyCode::Char(ch) => {
|
||||
// Check if Ctrl is enabled
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// If 'S', save bookmark as...
|
||||
match ch {
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Save bookmark as...
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Save bookmark as..."),
|
||||
AuthActivity::callback_save_bookmark,
|
||||
));
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
} else {
|
||||
match self.selected_field {
|
||||
InputField::Address => self.address.push(ch),
|
||||
InputField::Password => self.password.push(ch),
|
||||
InputField::Username => self.username.push(ch),
|
||||
InputField::Port => {
|
||||
// Value must be numeric
|
||||
if ch.is_numeric() {
|
||||
self.port.push(ch);
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
// If current field is Protocol handle event... (move element left)
|
||||
if self.selected_field == InputField::Protocol {
|
||||
self.protocol = match self.protocol {
|
||||
FileTransferProtocol::Sftp => FileTransferProtocol::Ftp(true), // End of list (wrap)
|
||||
FileTransferProtocol::Scp => FileTransferProtocol::Sftp,
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => FileTransferProtocol::Scp,
|
||||
true => FileTransferProtocol::Ftp(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
// If current field is Protocol handle event... ( move element right )
|
||||
if self.selected_field == InputField::Protocol {
|
||||
self.protocol = match self.protocol {
|
||||
FileTransferProtocol::Sftp => FileTransferProtocol::Scp,
|
||||
FileTransferProtocol::Scp => FileTransferProtocol::Ftp(false),
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => FileTransferProtocol::Ftp(true),
|
||||
true => FileTransferProtocol::Sftp, // End of list (wrap)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form_bookmarks
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Bookmarks
|
||||
pub(super) fn handle_input_event_mode_form_bookmarks(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Show quit dialog
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Tab => self.input_form = InputForm::AuthCredentials, // Move to Auth credentials
|
||||
KeyCode::Right => self.input_form = InputForm::Recents, // Move to recents
|
||||
KeyCode::Up => {
|
||||
// Move bookmarks index up
|
||||
if self.bookmarks_idx > 0 {
|
||||
self.bookmarks_idx -= 1;
|
||||
} else if let Some(hosts) = &self.bookmarks {
|
||||
// Put to last index (wrap)
|
||||
self.bookmarks_idx = hosts.bookmarks.len() - 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if let Some(hosts) = &self.bookmarks {
|
||||
let size: usize = hosts.bookmarks.len();
|
||||
// Check if can move down
|
||||
if self.bookmarks_idx + 1 >= size {
|
||||
// Move bookmarks index down
|
||||
self.bookmarks_idx = 0;
|
||||
} else {
|
||||
// Set index to first element (wrap)
|
||||
self.bookmarks_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
// Ask if user wants to delete bookmark
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
String::from("Are you sure you want to delete the selected bookmark?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Load bookmark
|
||||
self.load_bookmark(self.bookmarks_idx);
|
||||
// Set input form to Auth
|
||||
self.input_form = InputForm::AuthCredentials;
|
||||
// Set input field to password (very comfy)
|
||||
self.selected_field = InputField::Password;
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'E' | 'e' => {
|
||||
// Ask if user wants to delete bookmark; NOTE: same as <DEL>
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
String::from("Are you sure you want to delete the selected bookmark?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Save bookmark as...
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Save bookmark as..."),
|
||||
AuthActivity::callback_save_bookmark,
|
||||
));
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
},
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form_recents
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Recents
|
||||
pub(super) fn handle_input_event_mode_form_recents(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Show quit dialog
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Tab => self.input_form = InputForm::AuthCredentials, // Move to Auth credentials
|
||||
KeyCode::Left => self.input_form = InputForm::Bookmarks, // Move to bookmarks
|
||||
KeyCode::Up => {
|
||||
// Move bookmarks index up
|
||||
if self.recents_idx > 0 {
|
||||
self.recents_idx -= 1;
|
||||
} else if let Some(hosts) = &self.bookmarks {
|
||||
// Put to last index (wrap)
|
||||
self.recents_idx = hosts.recents.len() - 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if let Some(hosts) = &self.bookmarks {
|
||||
let size: usize = hosts.recents.len();
|
||||
// Check if can move down
|
||||
if self.recents_idx + 1 >= size {
|
||||
// Move bookmarks index down
|
||||
self.recents_idx = 0;
|
||||
} else {
|
||||
// Set index to first element (wrap)
|
||||
self.recents_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
// Ask if user wants to delete bookmark
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
String::from("Are you sure you want to delete the selected host?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Load bookmark
|
||||
self.load_recent(self.recents_idx);
|
||||
// Set input form to Auth
|
||||
self.input_form = InputForm::AuthCredentials;
|
||||
// Set input field to password (very comfy)
|
||||
self.selected_field = InputField::Password;
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'E' | 'e' => {
|
||||
// Ask if user wants to delete bookmark; NOTE: same as <DEL>
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
String::from("Are you sure you want to delete the selected host?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Save bookmark as...
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Save bookmark as..."),
|
||||
AuthActivity::callback_save_bookmark,
|
||||
));
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
},
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_text
|
||||
///
|
||||
/// Handler for input event when in popup mode
|
||||
pub(super) fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, ptype: PopupType) {
|
||||
match ptype {
|
||||
PopupType::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
|
||||
PopupType::Help => self.handle_input_event_mode_popup_help(ev),
|
||||
PopupType::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb),
|
||||
PopupType::YesNo(_, yes_cb, no_cb) => {
|
||||
self.handle_input_event_mode_popup_yesno(ev, yes_cb, no_cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_alert
|
||||
///
|
||||
/// Handle input event when the input mode is popup, and popup type is alert
|
||||
pub(super) fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) {
|
||||
// Only enter should be allowed here
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let KeyCode::Enter = key.code {
|
||||
self.input_mode = InputMode::Form; // Hide popup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_help
|
||||
///
|
||||
/// Input event handler for popup help
|
||||
pub(super) fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Enter | KeyCode::Esc => {
|
||||
// Set input mode back to form
|
||||
self.input_mode = InputMode::Form;
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_input
|
||||
///
|
||||
/// Input event handler for input popup
|
||||
pub(super) fn handle_input_event_mode_popup_input(
|
||||
&mut self,
|
||||
ev: &InputEvent,
|
||||
cb: OnInputSubmitCallback,
|
||||
) {
|
||||
// If enter, close popup, otherwise push chars to input
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Abort input
|
||||
// Clear current input text
|
||||
self.input_txt.clear();
|
||||
// Set mode back to form
|
||||
self.input_mode = InputMode::Form;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Submit
|
||||
let input_text: String = self.input_txt.clone();
|
||||
// Clear current input text
|
||||
self.input_txt.clear();
|
||||
// Set mode back to form BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.input_mode = InputMode::Form;
|
||||
// Call cb
|
||||
cb(self, input_text);
|
||||
}
|
||||
KeyCode::Char(ch) => self.input_txt.push(ch),
|
||||
KeyCode::Backspace => {
|
||||
let _ = self.input_txt.pop();
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_yesno
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
pub(super) fn handle_input_event_mode_popup_yesno(
|
||||
&mut self,
|
||||
ev: &InputEvent,
|
||||
yes_cb: DialogCallback,
|
||||
no_cb: DialogCallback,
|
||||
) {
|
||||
// If enter, close popup, otherwise move dialog option
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
// @! Set input mode to Form BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.input_mode = InputMode::Form;
|
||||
// Check if user selected yes or not
|
||||
match self.choice_opt {
|
||||
DialogYesNoOption::No => no_cb(self),
|
||||
DialogYesNoOption::Yes => yes_cb(self),
|
||||
}
|
||||
// Reset choice option to yes
|
||||
self.choice_opt = DialogYesNoOption::Yes;
|
||||
}
|
||||
KeyCode::Right => self.choice_opt = DialogYesNoOption::No, // Set to NO
|
||||
KeyCode::Left => self.choice_opt = DialogYesNoOption::Yes, // Set to YES
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
541
src/ui/activities/auth_activity/layout.rs
Normal file
541
src/ui/activities/auth_activity/layout.rs
Normal file
@@ -0,0 +1,541 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{
|
||||
AuthActivity, Context, DialogYesNoOption, FileTransferProtocol, InputField, InputForm,
|
||||
InputMode, PopupType,
|
||||
};
|
||||
|
||||
use crate::bookmarks::Bookmark;
|
||||
use crate::utils::align_text_center;
|
||||
|
||||
use tui::{
|
||||
layout::{Constraint, Corner, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### draw
|
||||
///
|
||||
/// Draw UI
|
||||
pub(super) fn draw(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal.draw(|f| {
|
||||
// Prepare chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(70), // Auth Form
|
||||
Constraint::Percentage(30), // Bookmarks
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// Create explorer chunks
|
||||
let auth_chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(chunks[0]);
|
||||
// Create bookmark chunks
|
||||
let bookmark_chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(chunks[1]);
|
||||
// Draw header
|
||||
f.render_widget(self.draw_header(), auth_chunks[0]);
|
||||
// Draw input fields
|
||||
f.render_widget(self.draw_remote_address(), auth_chunks[1]);
|
||||
f.render_widget(self.draw_remote_port(), auth_chunks[2]);
|
||||
f.render_widget(self.draw_protocol_select(), auth_chunks[3]);
|
||||
f.render_widget(self.draw_protocol_username(), auth_chunks[4]);
|
||||
f.render_widget(self.draw_protocol_password(), auth_chunks[5]);
|
||||
// Draw footer
|
||||
f.render_widget(self.draw_footer(), auth_chunks[6]);
|
||||
// Set cursor
|
||||
if let InputForm::AuthCredentials = self.input_form {
|
||||
match self.selected_field {
|
||||
InputField::Address => f.set_cursor(
|
||||
auth_chunks[1].x + self.address.width() as u16 + 1,
|
||||
auth_chunks[1].y + 1,
|
||||
),
|
||||
InputField::Port => f.set_cursor(
|
||||
auth_chunks[2].x + self.port.width() as u16 + 1,
|
||||
auth_chunks[2].y + 1,
|
||||
),
|
||||
InputField::Username => f.set_cursor(
|
||||
auth_chunks[4].x + self.username.width() as u16 + 1,
|
||||
auth_chunks[4].y + 1,
|
||||
),
|
||||
InputField::Password => f.set_cursor(
|
||||
auth_chunks[5].x + self.password_placeholder.width() as u16 + 1,
|
||||
auth_chunks[5].y + 1,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Draw bookmarks
|
||||
if let Some(tab) = self.draw_bookmarks_tab() {
|
||||
let mut bookmarks_state: ListState = ListState::default();
|
||||
bookmarks_state.select(Some(self.bookmarks_idx));
|
||||
f.render_stateful_widget(tab, bookmark_chunks[0], &mut bookmarks_state);
|
||||
}
|
||||
if let Some(tab) = self.draw_recents_tab() {
|
||||
let mut recents_state: ListState = ListState::default();
|
||||
recents_state.select(Some(self.recents_idx));
|
||||
f.render_stateful_widget(tab, bookmark_chunks[1], &mut recents_state);
|
||||
}
|
||||
// Draw popup
|
||||
if let InputMode::Popup(popup) = &self.input_mode {
|
||||
// Calculate popup size
|
||||
let (width, height): (u16, u16) = match popup {
|
||||
PopupType::Alert(_, _) => (50, 10),
|
||||
PopupType::Help => (50, 70),
|
||||
PopupType::Input(_, _) => (40, 10),
|
||||
PopupType::YesNo(_, _, _) => (30, 10),
|
||||
};
|
||||
let popup_area: Rect = self.draw_popup_area(f.size(), width, height);
|
||||
f.render_widget(Clear, popup_area); //this clears out the background
|
||||
match popup {
|
||||
PopupType::Alert(color, txt) => f.render_widget(
|
||||
self.draw_popup_alert(*color, txt.clone(), popup_area.width),
|
||||
popup_area,
|
||||
),
|
||||
PopupType::Help => f.render_widget(self.draw_popup_help(), popup_area),
|
||||
PopupType::Input(txt, _) => {
|
||||
f.render_widget(self.draw_popup_input(txt.clone()), popup_area);
|
||||
// Set cursor
|
||||
f.set_cursor(
|
||||
popup_area.x + self.input_txt.width() as u16 + 1,
|
||||
popup_area.y + 1,
|
||||
)
|
||||
}
|
||||
PopupType::YesNo(txt, _, _) => {
|
||||
f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.context = Some(ctx);
|
||||
}
|
||||
|
||||
/// ### draw_remote_address
|
||||
///
|
||||
/// Draw remote address block
|
||||
fn draw_remote_address(&self) -> Paragraph {
|
||||
Paragraph::new(self.address.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Address => Style::default().fg(Color::Yellow),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Remote address"),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_remote_port
|
||||
///
|
||||
/// Draw remote port block
|
||||
fn draw_remote_port(&self) -> Paragraph {
|
||||
Paragraph::new(self.port.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Port => Style::default().fg(Color::Cyan),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Remote port"))
|
||||
}
|
||||
|
||||
/// ### draw_protocol_select
|
||||
///
|
||||
/// Draw protocol select
|
||||
fn draw_protocol_select(&self) -> Tabs {
|
||||
let protocols: Vec<Spans> = vec![
|
||||
Spans::from("SFTP"),
|
||||
Spans::from("SCP"),
|
||||
Spans::from("FTP"),
|
||||
Spans::from("FTPS"),
|
||||
];
|
||||
let index: usize = match self.protocol {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => 2,
|
||||
true => 3,
|
||||
},
|
||||
};
|
||||
Tabs::new(protocols)
|
||||
.block(Block::default().borders(Borders::ALL).title("Protocol"))
|
||||
.select(index)
|
||||
.style(match self.selected_field {
|
||||
InputField::Protocol => Style::default().fg(Color::Green),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Green)
|
||||
.fg(Color::Black),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_protocol_username
|
||||
///
|
||||
/// Draw username block
|
||||
fn draw_protocol_username(&self) -> Paragraph {
|
||||
Paragraph::new(self.username.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Username => Style::default().fg(Color::Magenta),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Username"))
|
||||
}
|
||||
|
||||
/// ### draw_protocol_password
|
||||
///
|
||||
/// Draw password block
|
||||
fn draw_protocol_password(&mut self) -> Paragraph {
|
||||
// Create password secret
|
||||
self.password_placeholder = (0..self.password.width()).map(|_| "*").collect::<String>();
|
||||
Paragraph::new(self.password_placeholder.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Password => Style::default().fg(Color::LightBlue),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Password"))
|
||||
}
|
||||
|
||||
/// ### draw_header
|
||||
///
|
||||
/// Draw header
|
||||
fn draw_header(&self) -> Paragraph {
|
||||
Paragraph::new(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")
|
||||
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD))
|
||||
}
|
||||
|
||||
/// ### draw_footer
|
||||
///
|
||||
/// Draw authentication page footer
|
||||
fn draw_footer(&self) -> Paragraph {
|
||||
// Write header
|
||||
let (footer, h_style) = (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("<CTRL+H>", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to show keybindings"),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
);
|
||||
let mut footer_text = Text::from(Spans::from(footer));
|
||||
footer_text.patch_style(h_style);
|
||||
Paragraph::new(footer_text)
|
||||
}
|
||||
|
||||
/// ### draw_local_explorer
|
||||
///
|
||||
/// Draw local explorer list
|
||||
pub(super) fn draw_bookmarks_tab(&self) -> Option<List> {
|
||||
self.bookmarks.as_ref()?;
|
||||
let hosts: Vec<ListItem> = self
|
||||
.bookmarks
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.bookmarks
|
||||
.iter()
|
||||
.map(|(key, entry): (&String, &Bookmark)| {
|
||||
ListItem::new(Span::from(format!(
|
||||
"{} ({}://{}@{}:{})",
|
||||
key,
|
||||
entry.protocol.to_lowercase(),
|
||||
entry.username,
|
||||
entry.address,
|
||||
entry.port
|
||||
)))
|
||||
})
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.input_form {
|
||||
InputForm::Bookmarks => (Color::Black, Color::LightGreen),
|
||||
_ => (Color::Reset, Color::Reset),
|
||||
};
|
||||
Some(
|
||||
List::new(hosts)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(match self.input_form {
|
||||
InputForm::Bookmarks => Style::default().fg(Color::LightGreen),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.title("Bookmarks"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD)),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_local_explorer
|
||||
///
|
||||
/// Draw local explorer list
|
||||
pub(super) fn draw_recents_tab(&self) -> Option<List> {
|
||||
self.bookmarks.as_ref()?;
|
||||
let hosts: Vec<ListItem> = self
|
||||
.bookmarks
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.recents
|
||||
.values()
|
||||
.map(|entry: &Bookmark| {
|
||||
ListItem::new(Span::from(format!(
|
||||
"{}://{}@{}:{}",
|
||||
entry.protocol.to_lowercase(),
|
||||
entry.username,
|
||||
entry.address,
|
||||
entry.port
|
||||
)))
|
||||
})
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.input_form {
|
||||
InputForm::Recents => (Color::Black, Color::LightBlue),
|
||||
_ => (Color::Reset, Color::Reset),
|
||||
};
|
||||
Some(
|
||||
List::new(hosts)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(match self.input_form {
|
||||
InputForm::Recents => Style::default().fg(Color::LightBlue),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.title("Recent connections"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD)),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_popup_area
|
||||
///
|
||||
/// Draw popup area
|
||||
fn draw_popup_area(&self, area: Rect, width: u16, height: u16) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - height) / 2),
|
||||
Constraint::Percentage(height),
|
||||
Constraint::Percentage((100 - height) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - width) / 2),
|
||||
Constraint::Percentage(width),
|
||||
Constraint::Percentage((100 - width) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
|
||||
/// ### draw_popup_alert
|
||||
///
|
||||
/// Draw alert popup
|
||||
fn draw_popup_alert(&self, color: Color, text: String, width: u16) -> List {
|
||||
// Wraps texts
|
||||
let message_rows = textwrap::wrap(text.as_str(), width as usize);
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for msg in message_rows.iter() {
|
||||
lines.push(ListItem::new(Spans::from(align_text_center(msg, width))));
|
||||
}
|
||||
List::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(color))
|
||||
.title("Alert"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.style(Style::default().fg(color))
|
||||
}
|
||||
|
||||
/// ### draw_popup_input
|
||||
///
|
||||
/// Draw input popup
|
||||
pub(super) fn draw_popup_input(&self, text: String) -> Paragraph {
|
||||
Paragraph::new(self.input_txt.as_ref())
|
||||
.style(Style::default().fg(Color::White))
|
||||
.block(Block::default().borders(Borders::ALL).title(text))
|
||||
}
|
||||
|
||||
/// ### draw_popup_yesno
|
||||
///
|
||||
/// Draw yes/no select popup
|
||||
pub(super) fn draw_popup_yesno(&self, text: String) -> Tabs {
|
||||
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
||||
let index: usize = match self.choice_opt {
|
||||
DialogYesNoOption::Yes => 0,
|
||||
DialogYesNoOption::No => 1,
|
||||
};
|
||||
Tabs::new(choices)
|
||||
.block(Block::default().borders(Borders::ALL).title(text))
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Yellow),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_footer
|
||||
///
|
||||
/// Draw authentication page footer
|
||||
pub(super) fn draw_popup_help(&self) -> List {
|
||||
// Write header
|
||||
let cmds: Vec<ListItem> = vec![
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<ESC>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Quit TermSCP"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<TAB>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Switch input form and bookmarks"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<RIGHT/LEFT>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Change bookmark tab"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<UP/DOWN>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Move up/down in current tab"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<ENTER>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Submit"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<DEL>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Delete bookmark"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<E>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Delete selected bookmark"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+H>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Show help"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+S>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Save bookmark"),
|
||||
])),
|
||||
];
|
||||
List::new(cmds)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default())
|
||||
.title("Help"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
}
|
||||
}
|
||||
225
src/ui/activities/auth_activity/mod.rs
Normal file
225
src/ui/activities/auth_activity/mod.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Sub modules
|
||||
mod bookmarks;
|
||||
mod callbacks;
|
||||
mod input;
|
||||
mod layout;
|
||||
|
||||
// Dependencies
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
extern crate unicode_width;
|
||||
|
||||
// locals
|
||||
use super::{Activity, Context};
|
||||
use crate::bookmarks::UserHosts;
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
|
||||
// Includes
|
||||
use crossterm::event::Event as InputEvent;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use tui::style::Color;
|
||||
|
||||
// Types
|
||||
type DialogCallback = fn(&mut AuthActivity);
|
||||
type OnInputSubmitCallback = fn(&mut AuthActivity, String);
|
||||
|
||||
/// ### InputField
|
||||
///
|
||||
/// InputField describes the current input field to edit
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
enum InputField {
|
||||
Address,
|
||||
Port,
|
||||
Protocol,
|
||||
Username,
|
||||
Password,
|
||||
}
|
||||
|
||||
/// ### DialogYesNoOption
|
||||
///
|
||||
/// Current yes/no dialog option
|
||||
#[derive(std::cmp::PartialEq, Clone)]
|
||||
enum DialogYesNoOption {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
/// ### PopupType
|
||||
///
|
||||
/// PopupType describes the type of the popup displayed
|
||||
#[derive(Clone)]
|
||||
enum PopupType {
|
||||
Alert(Color, String), // Show a message displaying text with the provided color
|
||||
Help, // Help page
|
||||
Input(String, OnInputSubmitCallback), // Input description; Callback for submit
|
||||
YesNo(String, DialogCallback, DialogCallback), // Yes, no callback
|
||||
}
|
||||
|
||||
/// ### InputMode
|
||||
///
|
||||
/// InputMode describes the current input mode
|
||||
/// Each input mode handle the input events in a different way
|
||||
enum InputMode {
|
||||
Form,
|
||||
Popup(PopupType),
|
||||
}
|
||||
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
/// ### InputForm
|
||||
///
|
||||
/// InputForm describes the selected input form
|
||||
enum InputForm {
|
||||
AuthCredentials,
|
||||
Bookmarks,
|
||||
Recents,
|
||||
}
|
||||
|
||||
/// ### AuthActivity
|
||||
///
|
||||
/// AuthActivity is the data holder for the authentication activity
|
||||
pub struct AuthActivity {
|
||||
pub address: String,
|
||||
pub port: String,
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub submit: bool, // becomes true after user has submitted fields
|
||||
pub quit: bool, // Becomes true if user has pressed esc
|
||||
context: Option<Context>,
|
||||
bookmarks: Option<UserHosts>,
|
||||
selected_field: InputField, // Selected field in AuthCredentials Form
|
||||
input_mode: InputMode,
|
||||
input_form: InputForm,
|
||||
password_placeholder: String,
|
||||
redraw: bool, // Should ui actually be redrawned?
|
||||
input_txt: String, // Input text
|
||||
choice_opt: DialogYesNoOption, // Dialog popup selected option
|
||||
bookmarks_idx: usize, // Index of selected bookmark
|
||||
recents_idx: usize, // Index of selected recent
|
||||
}
|
||||
|
||||
impl Default for AuthActivity {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new AuthActivity
|
||||
pub fn new() -> AuthActivity {
|
||||
AuthActivity {
|
||||
address: String::new(),
|
||||
port: String::from("22"),
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
submit: false,
|
||||
quit: false,
|
||||
context: None,
|
||||
bookmarks: None,
|
||||
selected_field: InputField::Address,
|
||||
input_mode: InputMode::Form,
|
||||
input_form: InputForm::AuthCredentials,
|
||||
password_placeholder: String::new(),
|
||||
redraw: true, // True at startup
|
||||
input_txt: String::new(),
|
||||
choice_opt: DialogYesNoOption::Yes,
|
||||
bookmarks_idx: 0,
|
||||
recents_idx: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity for AuthActivity {
|
||||
/// ### on_create
|
||||
///
|
||||
/// `on_create` is the function which must be called to initialize the activity.
|
||||
/// `on_create` must initialize all the data structures used by the activity
|
||||
/// Context is taken from activity manager and will be released only when activity is destroyed
|
||||
fn on_create(&mut self, context: Context) {
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
let _ = self.context.as_mut().unwrap().terminal.clear();
|
||||
// Put raw mode on enabled
|
||||
let _ = enable_raw_mode();
|
||||
self.input_mode = InputMode::Form;
|
||||
// Read bookmarks
|
||||
if self.bookmarks.is_none() {
|
||||
self.read_bookmarks();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_draw
|
||||
///
|
||||
/// `on_draw` is the function which draws the graphical interface.
|
||||
/// This function must be called at each tick to refresh the interface
|
||||
fn on_draw(&mut self) {
|
||||
// Context must be something
|
||||
if self.context.is_none() {
|
||||
return;
|
||||
}
|
||||
// Read one event
|
||||
if let Ok(event) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
if let Some(event) = event {
|
||||
// Set redraw to true
|
||||
self.redraw = true;
|
||||
// Handle event
|
||||
self.handle_input_event(&event);
|
||||
}
|
||||
}
|
||||
// Redraw if necessary
|
||||
if self.redraw {
|
||||
// Draw
|
||||
self.draw();
|
||||
// Set redraw to false
|
||||
self.redraw = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_destroy
|
||||
///
|
||||
/// `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.
|
||||
/// This function finally releases the context
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Disable raw mode
|
||||
let _ = disable_raw_mode();
|
||||
self.context.as_ref()?;
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
Some(mut ctx) => {
|
||||
let _ = ctx.terminal.clear();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,20 +327,20 @@ impl FileTransferActivity {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let files: Vec<FsEntry> = self.local.files.clone();
|
||||
// Get file at index
|
||||
if let Some(entry) = files.get(self.local.index) {
|
||||
// Call send (upload)
|
||||
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), Some(input));
|
||||
// Get file and clone (due to mutable / immutable stuff...)
|
||||
if self.local.files.get(self.local.index).is_some() {
|
||||
let file: FsEntry = self.local.files.get(self.local.index).unwrap().clone();
|
||||
// Call upload; pass realfile, keep link name
|
||||
self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(input));
|
||||
}
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
let files: Vec<FsEntry> = self.remote.files.clone();
|
||||
// Get file at index
|
||||
if let Some(entry) = files.get(self.remote.index) {
|
||||
// Call receive (download)
|
||||
// Get file and clone (due to mutable / immutable stuff...)
|
||||
if self.remote.files.get(self.remote.index).is_some() {
|
||||
let file: FsEntry = self.remote.files.get(self.remote.index).unwrap().clone();
|
||||
// Call upload; pass realfile, keep link name
|
||||
self.filetransfer_recv(
|
||||
&entry.get_realfile(),
|
||||
&file.get_realfile(),
|
||||
self.context.as_ref().unwrap().local.pwd().as_path(),
|
||||
Some(input),
|
||||
);
|
||||
|
||||
@@ -219,6 +219,11 @@ impl FileTransferActivity {
|
||||
// Show file info
|
||||
self.input_mode = InputMode::Popup(PopupType::FileInfo);
|
||||
}
|
||||
'l' | 'L' => {
|
||||
// Reload file entries
|
||||
let pwd: PathBuf = self.context.as_ref().unwrap().local.pwd();
|
||||
self.local_scan(pwd.as_path());
|
||||
}
|
||||
'r' | 'R' => {
|
||||
// Rename
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
@@ -258,14 +263,14 @@ impl FileTransferActivity {
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Get files
|
||||
let files: Vec<FsEntry> = self.local.files.clone(); // Otherwise self is borrowed both as mutable and immutable...
|
||||
// Get file at index
|
||||
if let Some(entry) = files.get(self.local.index) {
|
||||
let name: String = entry.get_name();
|
||||
// Get file and clone (due to mutable / immutable stuff...)
|
||||
if self.local.files.get(self.local.index).is_some() {
|
||||
let file: FsEntry =
|
||||
self.local.files.get(self.local.index).unwrap().clone();
|
||||
let name: String = file.get_name();
|
||||
// Call upload; pass realfile, keep link name
|
||||
self.filetransfer_send(
|
||||
&entry.get_realfile(),
|
||||
&file.get_realfile(),
|
||||
wrkdir.as_path(),
|
||||
Some(name),
|
||||
);
|
||||
@@ -368,10 +373,6 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'q' | 'Q' => {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
}
|
||||
'e' | 'E' => {
|
||||
// Get file at index
|
||||
if let Some(entry) = self.remote.files.get(self.remote.index) {
|
||||
@@ -388,6 +389,13 @@ impl FileTransferActivity {
|
||||
))
|
||||
}
|
||||
}
|
||||
'd' | 'D' => {
|
||||
// Make directory
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert directory name"),
|
||||
FileTransferActivity::callback_mkdir,
|
||||
));
|
||||
}
|
||||
'g' | 'G' => {
|
||||
// Goto
|
||||
// Show input popup
|
||||
@@ -396,13 +404,6 @@ impl FileTransferActivity {
|
||||
FileTransferActivity::callback_change_directory,
|
||||
));
|
||||
}
|
||||
'd' | 'D' => {
|
||||
// Make directory
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert directory name"),
|
||||
FileTransferActivity::callback_mkdir,
|
||||
));
|
||||
}
|
||||
'h' | 'H' => {
|
||||
// Show help
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
@@ -411,6 +412,14 @@ impl FileTransferActivity {
|
||||
// Show file info
|
||||
self.input_mode = InputMode::Popup(PopupType::FileInfo);
|
||||
}
|
||||
'l' | 'L' => {
|
||||
// Reload file entries
|
||||
self.reload_remote_dir();
|
||||
}
|
||||
'q' | 'Q' => {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
}
|
||||
'r' | 'R' => {
|
||||
// Rename
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
@@ -444,15 +453,14 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
' ' => {
|
||||
// Get files
|
||||
let files: Vec<FsEntry> = self.remote.files.clone(); // Otherwise self is borrowed both as mutable and immutable...
|
||||
// Get file at index
|
||||
if let Some(entry) = files.get(self.remote.index) {
|
||||
// Preserve name
|
||||
let name: String = entry.get_name();
|
||||
// Call upload (use entry realfile; pass previous name)
|
||||
// Get file and clone (due to mutable / immutable stuff...)
|
||||
if self.remote.files.get(self.remote.index).is_some() {
|
||||
let file: FsEntry =
|
||||
self.remote.files.get(self.remote.index).unwrap().clone();
|
||||
let name: String = file.get_name();
|
||||
// Call upload; pass realfile, keep link name
|
||||
self.filetransfer_recv(
|
||||
&entry.get_realfile(),
|
||||
&file.get_realfile(),
|
||||
self.context.as_ref().unwrap().local.pwd().as_path(),
|
||||
Some(name),
|
||||
);
|
||||
@@ -637,7 +645,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_explorer_alert
|
||||
/// ### handle_input_event_mode_popup_progress
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
pub(super) fn handle_input_event_mode_popup_progress(&mut self, ev: &InputEvent) {
|
||||
@@ -652,14 +660,14 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_explorer_alert
|
||||
/// ### handle_input_event_mode_popup_wait
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
pub(super) fn handle_input_event_mode_popup_wait(&mut self, _ev: &InputEvent) {
|
||||
// There's nothing you can do here I guess... maybe ctrl+c in the future idk
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_explorer_alert
|
||||
/// ### handle_input_event_mode_popup_yesno
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
pub(super) fn handle_input_event_mode_popup_yesno(
|
||||
|
||||
@@ -28,7 +28,7 @@ use super::{
|
||||
Context, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputField,
|
||||
InputMode, LogLevel, LogRecord, PopupType,
|
||||
};
|
||||
use crate::utils::time_to_str;
|
||||
use crate::utils::{align_text_center, time_to_str};
|
||||
|
||||
use bytesize::ByteSize;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -56,8 +56,8 @@ impl FileTransferActivity {
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(20), // Explorer
|
||||
Constraint::Length(16), // Log
|
||||
Constraint::Percentage(70), // Explorer
|
||||
Constraint::Percentage(30), // Log
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
@@ -166,13 +166,18 @@ impl FileTransferActivity {
|
||||
.iter()
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.tab {
|
||||
FileExplorerTab::Local => (Color::Black, Color::LightYellow),
|
||||
_ => (Color::LightYellow, Color::Reset),
|
||||
};
|
||||
List::new(files)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(match self.input_field {
|
||||
InputField::Explorer => match self.tab {
|
||||
FileExplorerTab::Local => Style::default().fg(Color::Yellow),
|
||||
FileExplorerTab::Local => Style::default().fg(Color::LightYellow),
|
||||
_ => Style::default(),
|
||||
},
|
||||
_ => Style::default(),
|
||||
@@ -189,12 +194,7 @@ impl FileTransferActivity {
|
||||
)),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::LightYellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_style(Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD))
|
||||
}
|
||||
|
||||
/// ### draw_remote_explorer
|
||||
@@ -207,6 +207,11 @@ impl FileTransferActivity {
|
||||
.iter()
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.tab {
|
||||
FileExplorerTab::Remote => (Color::Black, Color::LightBlue),
|
||||
_ => (Color::LightBlue, Color::Reset),
|
||||
};
|
||||
List::new(files)
|
||||
.block(
|
||||
Block::default()
|
||||
@@ -230,12 +235,7 @@ impl FileTransferActivity {
|
||||
)),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightBlue)
|
||||
.fg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_style(Style::default().bg(bg).fg(fg).add_modifier(Modifier::BOLD))
|
||||
}
|
||||
|
||||
/// ### draw_log_list
|
||||
@@ -334,9 +334,7 @@ impl FileTransferActivity {
|
||||
let message_rows = textwrap::wrap(text.as_str(), width as usize);
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for msg in message_rows.iter() {
|
||||
lines.push(ListItem::new(Spans::from(
|
||||
FileTransferActivity::align_text_center(msg, width),
|
||||
)));
|
||||
lines.push(ListItem::new(Spans::from(align_text_center(msg, width))));
|
||||
}
|
||||
List::new(lines)
|
||||
.block(
|
||||
@@ -357,9 +355,7 @@ impl FileTransferActivity {
|
||||
let message_rows = textwrap::wrap(text.as_str(), width as usize);
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for msg in message_rows.iter() {
|
||||
lines.push(ListItem::new(Spans::from(
|
||||
FileTransferActivity::align_text_center(msg, width),
|
||||
)));
|
||||
lines.push(ListItem::new(Spans::from(align_text_center(msg, width))));
|
||||
}
|
||||
List::new(lines)
|
||||
.block(
|
||||
@@ -415,9 +411,7 @@ impl FileTransferActivity {
|
||||
let message_rows = textwrap::wrap(text.as_str(), width as usize);
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for msg in message_rows.iter() {
|
||||
lines.push(ListItem::new(Spans::from(
|
||||
FileTransferActivity::align_text_center(msg, width),
|
||||
)));
|
||||
lines.push(ListItem::new(Spans::from(align_text_center(msg, width))));
|
||||
}
|
||||
List::new(lines)
|
||||
.block(
|
||||
@@ -624,7 +618,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("disconnect"),
|
||||
Span::raw("Disconnect"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -654,7 +648,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("change explorer tab"),
|
||||
Span::raw("Change explorer tab"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -664,7 +658,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("move up/down in list"),
|
||||
Span::raw("Move up/down in list"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -674,7 +668,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("scroll up/down in list quickly"),
|
||||
Span::raw("Scroll up/down in list quickly"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -684,7 +678,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("enter directory"),
|
||||
Span::raw("Enter directory"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -694,7 +688,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("upload/download file"),
|
||||
Span::raw("Upload/download file"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -704,7 +698,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("delete file"),
|
||||
Span::raw("Delete file"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -714,7 +708,17 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("make directory"),
|
||||
Span::raw("Make directory"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<E>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Same as <DEL>"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -724,7 +728,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("goto path"),
|
||||
Span::raw("Goto path"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -734,7 +738,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("show help"),
|
||||
Span::raw("Show help"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -744,7 +748,17 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("show info about the selected file or directory"),
|
||||
Span::raw("Show info about the selected file or directory"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<L>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Reload directory content"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -764,7 +778,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("rename file"),
|
||||
Span::raw("Rename file"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -774,7 +788,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("go to parent directory"),
|
||||
Span::raw("Go to parent directory"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -784,7 +798,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("abort current file transfer"),
|
||||
Span::raw("Abort current file transfer"),
|
||||
])),
|
||||
];
|
||||
List::new(cmds)
|
||||
@@ -797,21 +811,6 @@ impl FileTransferActivity {
|
||||
.start_corner(Corner::TopLeft)
|
||||
}
|
||||
|
||||
/// align_text_center
|
||||
///
|
||||
/// Align text to center for a given width
|
||||
fn align_text_center(text: &str, width: u16) -> String {
|
||||
let indent_size: usize = match (width as usize) >= text.len() {
|
||||
// NOTE: The check prevents underflow
|
||||
true => (width as usize - text.len()) / 2,
|
||||
false => 0,
|
||||
};
|
||||
textwrap::indent(
|
||||
text,
|
||||
(0..indent_size).map(|_| " ").collect::<String>().as_str(),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### elide_wrkdir_path
|
||||
///
|
||||
/// Elide working directory path if longer than width + host.len
|
||||
|
||||
@@ -451,7 +451,7 @@ impl FileTransferActivity {
|
||||
last_input_event_fetch = Instant::now();
|
||||
}
|
||||
// Read till you can
|
||||
let mut buffer: [u8; 8192] = [0; 8192];
|
||||
let mut buffer: [u8; 65536] = [0; 65536];
|
||||
match rhnd.read(&mut buffer) {
|
||||
Ok(bytes_read) => {
|
||||
total_bytes_written += bytes_read;
|
||||
|
||||
@@ -45,6 +45,7 @@ impl InputHandler {
|
||||
/// ### fetch_events
|
||||
///
|
||||
/// Check if new events have been received from handler
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn fetch_events(&self) -> Result<Vec<Event>, ()> {
|
||||
let mut inbox: Vec<Event> = Vec::new();
|
||||
loop {
|
||||
|
||||
26
src/utils.rs
26
src/utils.rs
@@ -25,6 +25,7 @@
|
||||
|
||||
// Dependencies
|
||||
extern crate chrono;
|
||||
extern crate textwrap;
|
||||
extern crate whoami;
|
||||
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
@@ -241,6 +242,23 @@ pub fn lstime_to_systime(
|
||||
.unwrap_or(SystemTime::UNIX_EPOCH))
|
||||
}
|
||||
|
||||
/// align_text_center
|
||||
///
|
||||
/// Align text to center for a given width
|
||||
pub fn align_text_center(text: &str, width: u16) -> String {
|
||||
let indent_size: usize = match (width as usize) >= text.len() {
|
||||
// NOTE: The check prevents underflow
|
||||
true => (width as usize - text.len()) / 2,
|
||||
false => 0,
|
||||
};
|
||||
textwrap::indent(
|
||||
text,
|
||||
(0..indent_size).map(|_| " ").collect::<String>().as_str(),
|
||||
)
|
||||
.trim_end()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -392,4 +410,12 @@ mod tests {
|
||||
assert!(lstime_to_systime("Feb 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
|
||||
assert!(lstime_to_systime("Feb 15 25:32", "%b %d %Y", "%b %d %H:%M").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_align_text_center() {
|
||||
assert_eq!(
|
||||
align_text_center("hello world!", 24),
|
||||
String::from(" hello world!")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user