Merge branch 'bookmarks' into 0.2.0

This commit is contained in:
ChristianVisintin
2020-12-15 16:45:34 +01:00
25 changed files with 2395 additions and 659 deletions

3
.gitignore vendored
View File

@@ -13,3 +13,6 @@
**/*.rs.bk
# End of https://www.gitignore.io/api/rust
*.rpm
*.deb

View File

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

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

View File

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

View File

@@ -1,12 +1,12 @@
# TermSCP
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/ChristianVisintin/TermSCP.svg)](https://github.com/ChristianVisintin/TermSCP) [![Issues](https://img.shields.io/github/issues/ChristianVisintin/TermSCP.svg)](https://github.com/ChristianVisintin/TermSCP/issues) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.1.2-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/ChristianVisintin/TermSCP.svg)](https://github.com/ChristianVisintin/TermSCP) [![Issues](https://img.shields.io/github/issues/ChristianVisintin/TermSCP.svg)](https://github.com/ChristianVisintin/TermSCP/issues) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.1.3-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![Build](https://github.com/ChristianVisintin/TermSCP/workflows/Linux/badge.svg)](https://github.com/ChristianVisintin/TermSCP/actions) [![Build](https://github.com/ChristianVisintin/TermSCP/workflows/MacOS/badge.svg)](https://github.com/ChristianVisintin/TermSCP/actions) [![Build](https://github.com/ChristianVisintin/TermSCP/workflows/Windows/badge.svg)](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
View 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
View 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
View 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
View 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
}
}

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ use std::time::Duration;
// Include
mod activity_manager;
mod bookmarks;
mod filetransfer;
mod fs;
mod host;

View File

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

View 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
}
}
}

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

View 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 */ }
}
}
}
}

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

View 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,
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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