mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20872829f1 | ||
|
|
2d37607d2c | ||
|
|
c9c35d027b | ||
|
|
11475b64ea | ||
|
|
53249c8bc5 | ||
|
|
843c5ab6d0 | ||
|
|
66f17c93c2 | ||
|
|
69df9f0aa9 | ||
|
|
5c5a93cc41 | ||
|
|
040b684398 | ||
|
|
d75867ed7b | ||
|
|
e36ae7e32d | ||
|
|
4aa262a273 | ||
|
|
7e6044a41b | ||
|
|
145b778ff3 | ||
|
|
ca46c872cf | ||
|
|
98e3866447 | ||
|
|
95ab3daa86 | ||
|
|
93977cc714 | ||
|
|
b3537afd2e | ||
|
|
939f741a1b | ||
|
|
5a96091258 | ||
|
|
2d20f0c534 | ||
|
|
8f3e416144 |
6
.github/workflows/linux.yml
vendored
6
.github/workflows/linux.yml
vendored
@@ -1,10 +1,6 @@
|
||||
name: Linux
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
8
.github/workflows/macos.yml
vendored
8
.github/workflows/macos.yml
vendored
@@ -1,10 +1,6 @@
|
||||
name: MacOS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -12,7 +8,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: macos-10.14
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
6
.github/workflows/windows.yml
vendored
6
.github/workflows/windows.yml
vendored
@@ -1,10 +1,6 @@
|
||||
name: Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,10 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
- [Changelog](#changelog)
|
||||
- [0.1.1](#011)
|
||||
- [0.1.0](#010)
|
||||
|
||||
---
|
||||
|
||||
## 0.1.1
|
||||
|
||||
Released on 10/12/2020
|
||||
|
||||
- enhancements:
|
||||
- password prompt: ask before performing terminal clear
|
||||
- file explorer:
|
||||
- file names are now sorted ignoring capital letters
|
||||
- file names longer than 23, are now cut to 20 and followed by `...`
|
||||
- paths which exceed tab size in explorer are elided with the following formato `ANCESTOR[1]/.../PARENT/DIRNAME`
|
||||
- keybindings:
|
||||
- `I`: show info about selected file or directory
|
||||
- Removed `CTRL`; just use keys now.
|
||||
- bugfix:
|
||||
- prevent panic in set_progress, for progress values `> 100.0 or < 0.0`
|
||||
- Fixed FTP get, which didn't finalize the reader
|
||||
- dependencies:
|
||||
- updated `textwrap` to `0.13.0`
|
||||
- updated `ftp4` to `4.0.1`
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Released on 06/12/2020
|
||||
|
||||
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -137,9 +137,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "ftp4"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "412a5da44e30489a1790749f34044d53bbc4ff0cf8d789ee37be4b6befeb7c23"
|
||||
checksum = "6318bd155755b6e07ccb7bf8e5b1b7cb221c74fff7c6440692ef38eb2ec1d42c"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -603,6 +603,12 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1bc737c97d093feb72e67f4926d9b22d717ce8580cd25f0ce86d74e859c466d"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.3.17"
|
||||
@@ -654,7 +660,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "termscp"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"bytesize",
|
||||
"chrono",
|
||||
@@ -676,10 +682,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.12.1"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
|
||||
checksum = "b1bca196a5c5a7bc57a5c92809cf5670e16bcbca3bf0d09ef47150bf97221f6f"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "termscp"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
authors = ["Christian Visintin"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0"
|
||||
@@ -16,7 +16,7 @@ readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.18.2"
|
||||
ftp4 = { version = "^4.0.0", features = ["secure"] }
|
||||
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 }
|
||||
@@ -25,7 +25,7 @@ rpassword = "5.0.0"
|
||||
unicode-width = "0.1.7"
|
||||
chrono = "0.4.19"
|
||||
bytesize = "1.0.1"
|
||||
textwrap = "0.12.1"
|
||||
textwrap = "0.13.0"
|
||||
regex = "1.4.2"
|
||||
lazy_static = "1.4.0"
|
||||
hostname = "0.3.1"
|
||||
|
||||
33
README.md
33
README.md
@@ -1,12 +1,12 @@
|
||||
# TermSCP
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://github.com/ChristianVisintin/TermSCP) [](https://github.com/ChristianVisintin/TermSCP/issues) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://github.com/ChristianVisintin/TermSCP) [](https://github.com/ChristianVisintin/TermSCP/issues) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
|
||||
[](https://github.com/ChristianVisintin/TermSCP/actions) [](https://github.com/ChristianVisintin/TermSCP/actions) [](https://github.com/ChristianVisintin/TermSCP/actions)
|
||||
|
||||
~ Basically, WinSCP on a terminal ~
|
||||
Developed by Christian Visintin
|
||||
Current version: 0.1.0 (06/12/2020)
|
||||
Current version: 0.1.1 (10/12/2020)
|
||||
|
||||
---
|
||||
|
||||
@@ -37,7 +37,7 @@ Current version: 0.1.0 (06/12/2020)
|
||||
|
||||
## About TermSCP 🖥
|
||||
|
||||
TermSCP is basically a porting of WinSCP to terminal. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It works both on **Linux**, **MacOS**, **UNIX** and **Windows** and supports SFTP, SCP, FTP and FTPS.
|
||||
TermSCP is basically a porting of WinSCP to terminal. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It works both on **Linux**, **MacOS**, **BSD** and **Windows** and supports SFTP, SCP, FTP and FTPS.
|
||||
|
||||

|
||||
|
||||
@@ -45,7 +45,7 @@ TermSCP is basically a porting of WinSCP to terminal. So basically is a terminal
|
||||
|
||||
### Why TermSCP 🤔
|
||||
|
||||
It happens very often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me then to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there midnight commander too, but actually I don't like it very much tbh (and doesn't support scp).
|
||||
It happens quite often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me then to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there midnight commander too, but actually I don't like it very much tbh (and hasn't a decent support for scp).
|
||||
|
||||
## Features 🎁
|
||||
|
||||
@@ -54,7 +54,7 @@ It happens very often to me, when using SCP at work to forget the path of a file
|
||||
- SCP
|
||||
- FTP and FTPS
|
||||
- Practical user interface to explore and operate on the remote and on the local machine file system
|
||||
- Compatible with Windows, Linux, UNIX and MacOS
|
||||
- Compatible with Windows, Linux, BSD and MacOS
|
||||
- Written in Rust
|
||||
- Easy to extend with new file transfers protocols
|
||||
|
||||
@@ -74,8 +74,8 @@ cargo install termscp
|
||||
|
||||
### Deb package 📦
|
||||
|
||||
Get `deb` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.0_amd64.deb)
|
||||
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.0_amd64.deb`
|
||||
Get `deb` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.1_amd64.deb)
|
||||
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.1_amd64.deb`
|
||||
|
||||
then install through dpkg:
|
||||
|
||||
@@ -87,8 +87,8 @@ gdebi termscp_*.deb
|
||||
|
||||
### RPM Package 📦
|
||||
|
||||
Get `rpm` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.0-1.x86_64.rpm)
|
||||
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.0-1.x86_64.rpm`
|
||||
Get `rpm` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.1-1.x86_64.rpm)
|
||||
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.1-1.x86_64.rpm`
|
||||
|
||||
then install through rpm:
|
||||
|
||||
@@ -106,7 +106,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.0.nupkg)
|
||||
Alternatively you can download the ZIP file from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp.0.1.1.nupkg)
|
||||
|
||||
and then with PowerShell started with administrator previleges, run:
|
||||
|
||||
@@ -193,12 +193,13 @@ Password can be basically provided through 3 ways when address argument is provi
|
||||
| `<PGDOWN>` | Move down in selected list by 8 rows |
|
||||
| `<ENTER>` | Enter directory |
|
||||
| `<SPACE>` | Upload / download selected file |
|
||||
| `<CTRL+D>` | Make directory |
|
||||
| `<CTRL+G>` | Go to supplied path |
|
||||
| `<CTRL+H>` | Show help |
|
||||
| `<CTRL+Q>` | Quit TermSCP |
|
||||
| `<CTRL+R>` | Rename file |
|
||||
| `<CTRL+U>` | Go to parent directory |
|
||||
| `<D>` | Make directory |
|
||||
| `<G>` | Go to supplied path |
|
||||
| `<H>` | Show help |
|
||||
| `<H>` | Show info about selected file or directory |
|
||||
| `<Q>` | Quit TermSCP |
|
||||
| `<R>` | Rename file |
|
||||
| `<U>` | Go to parent directory |
|
||||
| `<CANC>` | Delete file |
|
||||
|
||||
---
|
||||
|
||||
@@ -574,8 +574,19 @@ impl FileTransfer for FtpFileTransfer {
|
||||
/// The purpose of this method is to finalize the connection with the peer when reading data.
|
||||
/// This mighe be necessary for some protocols.
|
||||
/// You must call this method each time you want to finalize the read of the remote file.
|
||||
fn on_recv(&mut self, _readable: Box<dyn Read>) -> Result<(), FileTransferError> {
|
||||
Ok(())
|
||||
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError> {
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.finalize_get(readable) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
format!("{}", err),
|
||||
)),
|
||||
},
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
230
src/fs/mod.rs
230
src/fs/mod.rs
@@ -27,7 +27,7 @@ extern crate bytesize;
|
||||
#[cfg(any(unix, macos, linux))]
|
||||
extern crate users;
|
||||
|
||||
use crate::utils::time_to_str;
|
||||
use crate::utils::{fmt_pex, time_to_str};
|
||||
|
||||
use bytesize::ByteSize;
|
||||
use std::path::PathBuf;
|
||||
@@ -101,51 +101,7 @@ impl std::fmt::Display for FsEntry {
|
||||
match dir.unix_pex {
|
||||
None => mode.push_str("?????????"),
|
||||
Some((owner, group, others)) => {
|
||||
let read: u8 = (owner >> 2) & 0x1;
|
||||
let write: u8 = (owner >> 1) & 0x1;
|
||||
let exec: u8 = owner & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (group >> 2) & 0x1;
|
||||
let write: u8 = (group >> 1) & 0x1;
|
||||
let exec: u8 = group & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (others >> 2) & 0x1;
|
||||
let write: u8 = (others >> 1) & 0x1;
|
||||
let exec: u8 = others & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(fmt_pex(owner, group, others).as_str())
|
||||
}
|
||||
}
|
||||
// Get username
|
||||
@@ -170,14 +126,15 @@ impl std::fmt::Display for FsEntry {
|
||||
let size: String = String::from("4096");
|
||||
// Get date
|
||||
let datetime: String = time_to_str(dir.last_change_time, "%b %d %Y %H:%M");
|
||||
// Set file name (or elide if too long)
|
||||
let dir_name: String = match dir.name.len() >= 24 {
|
||||
false => dir.name.clone(),
|
||||
true => format!("{}...", &dir.name.as_str()[0..20]),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
|
||||
dir.name.as_str(),
|
||||
mode,
|
||||
username,
|
||||
size,
|
||||
datetime
|
||||
dir_name, mode, username, size, datetime
|
||||
)
|
||||
}
|
||||
FsEntry::File(file) => {
|
||||
@@ -191,51 +148,7 @@ impl std::fmt::Display for FsEntry {
|
||||
match file.unix_pex {
|
||||
None => mode.push_str("?????????"),
|
||||
Some((owner, group, others)) => {
|
||||
let read: u8 = (owner >> 2) & 0x1;
|
||||
let write: u8 = (owner >> 1) & 0x1;
|
||||
let exec: u8 = owner & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (group >> 2) & 0x1;
|
||||
let write: u8 = (group >> 1) & 0x1;
|
||||
let exec: u8 = group & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (others >> 2) & 0x1;
|
||||
let write: u8 = (others >> 1) & 0x1;
|
||||
let exec: u8 = others & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(fmt_pex(owner, group, others).as_str())
|
||||
}
|
||||
}
|
||||
// Get username
|
||||
@@ -260,14 +173,15 @@ impl std::fmt::Display for FsEntry {
|
||||
let size: ByteSize = ByteSize(file.size as u64);
|
||||
// Get date
|
||||
let datetime: String = time_to_str(file.last_change_time, "%b %d %Y %H:%M");
|
||||
// Set file name (or elide if too long)
|
||||
let file_name: String = match file.name.len() >= 24 {
|
||||
false => file.name.clone(),
|
||||
true => format!("{}...", &file.name.as_str()[0..20]),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
|
||||
file.name.as_str(),
|
||||
mode,
|
||||
username,
|
||||
size,
|
||||
datetime
|
||||
file_name, mode, username, size, datetime
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -291,51 +205,7 @@ impl std::fmt::Display for FsEntry {
|
||||
match dir.unix_pex {
|
||||
None => mode.push_str("?????????"),
|
||||
Some((owner, group, others)) => {
|
||||
let read: u8 = (owner >> 2) & 0x1;
|
||||
let write: u8 = (owner >> 1) & 0x1;
|
||||
let exec: u8 = owner & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (group >> 2) & 0x1;
|
||||
let write: u8 = (group >> 1) & 0x1;
|
||||
let exec: u8 = group & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (others >> 2) & 0x1;
|
||||
let write: u8 = (others >> 1) & 0x1;
|
||||
let exec: u8 = others & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(fmt_pex(owner, group, others).as_str())
|
||||
}
|
||||
}
|
||||
// Get username
|
||||
@@ -354,14 +224,15 @@ impl std::fmt::Display for FsEntry {
|
||||
let size: String = String::from("4096");
|
||||
// Get date
|
||||
let datetime: String = time_to_str(dir.last_change_time, "%b %d %Y %H:%M");
|
||||
// Set file name (or elide if too long)
|
||||
let dir_name: String = match dir.name.len() >= 24 {
|
||||
false => dir.name.clone(),
|
||||
true => format!("{}...", &dir.name.as_str()[0..20]),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
|
||||
dir.name.as_str(),
|
||||
mode,
|
||||
username,
|
||||
size,
|
||||
datetime
|
||||
dir_name, mode, username, size, datetime
|
||||
)
|
||||
}
|
||||
FsEntry::File(file) => {
|
||||
@@ -375,51 +246,7 @@ impl std::fmt::Display for FsEntry {
|
||||
match file.unix_pex {
|
||||
None => mode.push_str("?????????"),
|
||||
Some((owner, group, others)) => {
|
||||
let read: u8 = (owner >> 2) & 0x1;
|
||||
let write: u8 = (owner >> 1) & 0x1;
|
||||
let exec: u8 = owner & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (group >> 2) & 0x1;
|
||||
let write: u8 = (group >> 1) & 0x1;
|
||||
let exec: u8 = group & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (others >> 2) & 0x1;
|
||||
let write: u8 = (others >> 1) & 0x1;
|
||||
let exec: u8 = others & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(fmt_pex(owner, group, others).as_str())
|
||||
}
|
||||
}
|
||||
// Get username
|
||||
@@ -438,14 +265,15 @@ impl std::fmt::Display for FsEntry {
|
||||
let size: ByteSize = ByteSize(file.size as u64);
|
||||
// Get date
|
||||
let datetime: String = time_to_str(file.last_change_time, "%b %d %Y %H:%M");
|
||||
// Set file name (or elide if too long)
|
||||
let file_name: String = match file.name.len() >= 24 {
|
||||
false => file.name.clone(),
|
||||
true => format!("{}...", &file.name.as_str()[0..20]),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
|
||||
file.name.as_str(),
|
||||
mode,
|
||||
username,
|
||||
size,
|
||||
datetime
|
||||
file_name, mode, username, size, datetime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
23
src/main.rs
23
src/main.rs
@@ -50,7 +50,7 @@ use filetransfer::FileTransferProtocol;
|
||||
/// Print usage
|
||||
|
||||
fn print_usage(opts: Options) {
|
||||
let brief = format!("Usage: termscp [Options]... [protocol:user@address:port]");
|
||||
let brief = format!("Usage: termscp [options]... [protocol://user@address:port]");
|
||||
print!("{}", opts.usage(&brief));
|
||||
println!("\nPlease, report issues to <https://github.com/ChristianVisintin/TermSCP>");
|
||||
}
|
||||
@@ -134,17 +134,9 @@ fn main() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => PathBuf::from("/"),
|
||||
};
|
||||
// Create activity manager
|
||||
let mut manager: ActivityManager = match ActivityManager::new(&wrkdir, ticks) {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
eprintln!("Invalid directory '{}'", wrkdir.display());
|
||||
std::process::exit(255);
|
||||
}
|
||||
};
|
||||
// Initialize client if necessary
|
||||
let mut start_activity: NextActivity = NextActivity::Authentication;
|
||||
if let Some(address) = address {
|
||||
if address.is_some() {
|
||||
if password.is_none() {
|
||||
// Ask password if unspecified
|
||||
password = match rpassword::read_password_from_tty(Some("Password: ")) {
|
||||
@@ -163,6 +155,17 @@ fn main() {
|
||||
}
|
||||
// In this case the first activity will be FileTransfer
|
||||
start_activity = NextActivity::FileTransfer;
|
||||
}
|
||||
// Create activity manager (and context too)
|
||||
let mut manager: ActivityManager = match ActivityManager::new(&wrkdir, ticks) {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
eprintln!("Invalid directory '{}'", wrkdir.display());
|
||||
std::process::exit(255);
|
||||
}
|
||||
};
|
||||
// Set file transfer params if set
|
||||
if let Some(address) = address {
|
||||
manager.set_filetransfer_params(address, port, protocol, username, password);
|
||||
}
|
||||
// Run
|
||||
|
||||
@@ -24,7 +24,7 @@ use super::{
|
||||
InputField, InputMode, LogLevel, OnInputSubmitCallback, PopupType,
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use crossterm::event::KeyCode;
|
||||
use std::path::PathBuf;
|
||||
use tui::style::Color;
|
||||
|
||||
@@ -187,69 +187,53 @@ impl FileTransferActivity {
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'q' | 'Q' => {
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
}
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
}
|
||||
'g' | 'G' => {
|
||||
// Goto
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Show input popup
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Change working directory"),
|
||||
FileTransferActivity::callback_change_directory,
|
||||
));
|
||||
}
|
||||
// Show input popup
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Change working directory"),
|
||||
FileTransferActivity::callback_change_directory,
|
||||
));
|
||||
}
|
||||
'd' | 'D' => {
|
||||
// Make directory
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert directory name"),
|
||||
FileTransferActivity::callback_mkdir,
|
||||
));
|
||||
}
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert directory name"),
|
||||
FileTransferActivity::callback_mkdir,
|
||||
));
|
||||
}
|
||||
'h' | 'H' => {
|
||||
// Show help
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
}
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
}
|
||||
'i' | 'I' => {
|
||||
// Show file info
|
||||
self.input_mode = InputMode::Popup(PopupType::FileInfo);
|
||||
}
|
||||
'r' | 'R' => {
|
||||
// Rename
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert new name"),
|
||||
FileTransferActivity::callback_rename,
|
||||
));
|
||||
}
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert new name"),
|
||||
FileTransferActivity::callback_rename,
|
||||
));
|
||||
}
|
||||
's' | 'S' => {
|
||||
// Save as...
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Ask for input
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Save as..."),
|
||||
FileTransferActivity::callback_save_as,
|
||||
));
|
||||
}
|
||||
// Ask for input
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Save as..."),
|
||||
FileTransferActivity::callback_save_as,
|
||||
));
|
||||
}
|
||||
'u' | 'U' => {
|
||||
// Go to parent directory
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Get pwd
|
||||
let path: PathBuf = self.context.as_ref().unwrap().local.pwd();
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.local_changedir(parent, true);
|
||||
}
|
||||
// Get pwd
|
||||
let path: PathBuf = self.context.as_ref().unwrap().local.pwd();
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.local_changedir(parent, true);
|
||||
}
|
||||
}
|
||||
' ' => {
|
||||
@@ -403,78 +387,62 @@ impl FileTransferActivity {
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'q' | 'Q' => {
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
}
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
}
|
||||
'g' | 'G' => {
|
||||
// Goto
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Show input popup
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Change working directory"),
|
||||
FileTransferActivity::callback_change_directory,
|
||||
));
|
||||
}
|
||||
// Show input popup
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Change working directory"),
|
||||
FileTransferActivity::callback_change_directory,
|
||||
));
|
||||
}
|
||||
'd' | 'D' => {
|
||||
// Make directory
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert directory name"),
|
||||
FileTransferActivity::callback_mkdir,
|
||||
));
|
||||
}
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert directory name"),
|
||||
FileTransferActivity::callback_mkdir,
|
||||
));
|
||||
}
|
||||
'h' | 'H' => {
|
||||
// Show help
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
}
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
}
|
||||
'i' | 'I' => {
|
||||
// Show file info
|
||||
self.input_mode = InputMode::Popup(PopupType::FileInfo);
|
||||
}
|
||||
'r' | 'R' => {
|
||||
// Rename
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert new name"),
|
||||
FileTransferActivity::callback_rename,
|
||||
));
|
||||
}
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert new name"),
|
||||
FileTransferActivity::callback_rename,
|
||||
));
|
||||
}
|
||||
's' | 'S' => {
|
||||
// Save as...
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Ask for input
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Save as..."),
|
||||
FileTransferActivity::callback_save_as,
|
||||
));
|
||||
}
|
||||
// Ask for input
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Save as..."),
|
||||
FileTransferActivity::callback_save_as,
|
||||
));
|
||||
}
|
||||
'u' | 'U' => {
|
||||
// Go to parent directory
|
||||
// If ctrl is enabled...
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Get pwd
|
||||
match self.client.pwd() {
|
||||
Ok(path) => {
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.remote_changedir(parent, true);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not change working directory: {}", err),
|
||||
))
|
||||
// Get pwd
|
||||
match self.client.pwd() {
|
||||
Ok(path) => {
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.remote_changedir(parent, true);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not change working directory: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
' ' => {
|
||||
@@ -549,10 +517,8 @@ impl FileTransferActivity {
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'q' | 'Q' => {
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
}
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
},
|
||||
@@ -569,6 +535,7 @@ impl FileTransferActivity {
|
||||
pub(super) fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, popup: PopupType) {
|
||||
match popup {
|
||||
PopupType::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
|
||||
PopupType::FileInfo => self.handle_input_event_mode_popup_fileinfo(ev),
|
||||
PopupType::Help => self.handle_input_event_mode_popup_help(ev),
|
||||
PopupType::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev),
|
||||
PopupType::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb),
|
||||
@@ -599,6 +566,25 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_fileinfo
|
||||
///
|
||||
/// Input event handler for popup fileinfo
|
||||
pub(super) fn handle_input_event_mode_popup_fileinfo(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup
|
||||
match ev {
|
||||
InputEvent::Key(key) => {
|
||||
match key.code {
|
||||
KeyCode::Enter | KeyCode::Esc => {
|
||||
// Set input mode back to explorer
|
||||
self.input_mode = InputMode::Explorer;
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_help
|
||||
///
|
||||
/// Input event handler for popup help
|
||||
|
||||
@@ -19,13 +19,19 @@
|
||||
*
|
||||
*/
|
||||
|
||||
extern crate bytesize;
|
||||
extern crate hostname;
|
||||
#[cfg(any(unix, macos, linux))]
|
||||
extern crate users;
|
||||
|
||||
use super::{
|
||||
Context, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputField,
|
||||
InputMode, LogLevel, LogRecord, PopupType,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use crate::utils::time_to_str;
|
||||
|
||||
use bytesize::ByteSize;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tui::{
|
||||
layout::{Constraint, Corner, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -33,6 +39,8 @@ use tui::{
|
||||
widgets::{Block, Borders, Clear, Gauge, List, ListItem, ListState, Paragraph, Tabs},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
#[cfg(any(unix, macos, linux))]
|
||||
use users::{get_group_by_gid, get_user_by_uid};
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### draw
|
||||
@@ -67,7 +75,7 @@ impl FileTransferActivity {
|
||||
remote_state.select(Some(self.remote.index));
|
||||
// Draw tabs
|
||||
f.render_stateful_widget(
|
||||
self.draw_local_explorer(local_wrkdir),
|
||||
self.draw_local_explorer(local_wrkdir, tabs_chunks[0].width),
|
||||
tabs_chunks[0],
|
||||
&mut localhost_state,
|
||||
);
|
||||
@@ -77,7 +85,7 @@ impl FileTransferActivity {
|
||||
Err(_) => PathBuf::from("/"),
|
||||
};
|
||||
f.render_stateful_widget(
|
||||
self.draw_remote_explorer(remote_wrkdir),
|
||||
self.draw_remote_explorer(remote_wrkdir, tabs_chunks[1].width),
|
||||
tabs_chunks[1],
|
||||
&mut remote_state,
|
||||
);
|
||||
@@ -96,8 +104,9 @@ impl FileTransferActivity {
|
||||
let (width, height): (u16, u16) = match popup {
|
||||
PopupType::Alert(_, _) => (50, 10),
|
||||
PopupType::Fatal(_) => (50, 10),
|
||||
PopupType::FileInfo => (50, 50),
|
||||
PopupType::Help => (50, 70),
|
||||
PopupType::Input(_, _) => (30, 10),
|
||||
PopupType::Input(_, _) => (40, 10),
|
||||
PopupType::Progress(_) => (40, 10),
|
||||
PopupType::Wait(_) => (50, 10),
|
||||
PopupType::YesNo(_, _, _) => (30, 10),
|
||||
@@ -113,6 +122,7 @@ impl FileTransferActivity {
|
||||
self.draw_popup_fatal(txt.clone(), popup_area.width),
|
||||
popup_area,
|
||||
),
|
||||
PopupType::FileInfo => f.render_widget(self.draw_popup_fileinfo(), 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);
|
||||
@@ -141,7 +151,7 @@ impl FileTransferActivity {
|
||||
/// ### draw_local_explorer
|
||||
///
|
||||
/// Draw local explorer list
|
||||
pub(super) fn draw_local_explorer(&self, local_wrkdir: PathBuf) -> List {
|
||||
pub(super) fn draw_local_explorer(&self, local_wrkdir: PathBuf, width: u16) -> List {
|
||||
let hostname: String = match hostname::get() {
|
||||
Ok(h) => String::from(h.as_os_str().to_string_lossy()),
|
||||
Err(_) => String::from("localhost"),
|
||||
@@ -163,7 +173,16 @@ impl FileTransferActivity {
|
||||
},
|
||||
_ => Style::default(),
|
||||
})
|
||||
.title(format!("{}:{} ", hostname, local_wrkdir.display())),
|
||||
.title(format!(
|
||||
"{}:{} ",
|
||||
hostname,
|
||||
FileTransferActivity::elide_wrkdir_path(
|
||||
local_wrkdir.as_path(),
|
||||
hostname.as_str(),
|
||||
width
|
||||
)
|
||||
.display()
|
||||
)),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
@@ -176,7 +195,7 @@ impl FileTransferActivity {
|
||||
/// ### draw_remote_explorer
|
||||
///
|
||||
/// Draw remote explorer list
|
||||
pub(super) fn draw_remote_explorer(&self, remote_wrkdir: PathBuf) -> List {
|
||||
pub(super) fn draw_remote_explorer(&self, remote_wrkdir: PathBuf, width: u16) -> List {
|
||||
let files: Vec<ListItem> = self
|
||||
.remote
|
||||
.files
|
||||
@@ -197,7 +216,12 @@ impl FileTransferActivity {
|
||||
.title(format!(
|
||||
"{}:{} ",
|
||||
self.params.address,
|
||||
remote_wrkdir.display()
|
||||
FileTransferActivity::elide_wrkdir_path(
|
||||
remote_wrkdir.as_path(),
|
||||
self.params.address.as_str(),
|
||||
width
|
||||
)
|
||||
.display()
|
||||
)),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
@@ -305,7 +329,7 @@ impl FileTransferActivity {
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for msg in message_rows.iter() {
|
||||
lines.push(ListItem::new(Spans::from(
|
||||
self.align_text_center(msg, width),
|
||||
FileTransferActivity::align_text_center(msg, width),
|
||||
)));
|
||||
}
|
||||
List::new(lines)
|
||||
@@ -328,7 +352,7 @@ impl FileTransferActivity {
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for msg in message_rows.iter() {
|
||||
lines.push(ListItem::new(Spans::from(
|
||||
self.align_text_center(msg, width),
|
||||
FileTransferActivity::align_text_center(msg, width),
|
||||
)));
|
||||
}
|
||||
List::new(lines)
|
||||
@@ -386,7 +410,7 @@ impl FileTransferActivity {
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for msg in message_rows.iter() {
|
||||
lines.push(ListItem::new(Spans::from(
|
||||
self.align_text_center(msg, width),
|
||||
FileTransferActivity::align_text_center(msg, width),
|
||||
)));
|
||||
}
|
||||
List::new(lines)
|
||||
@@ -420,6 +444,225 @@ impl FileTransferActivity {
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_popup_fileinfo
|
||||
///
|
||||
/// Draw popup containing info about selected fsentry
|
||||
pub(super) fn draw_popup_fileinfo(&self) -> List {
|
||||
let mut info: Vec<ListItem> = Vec::new();
|
||||
// Get current fsentry
|
||||
let fsentry: Option<&FsEntry> = match self.tab {
|
||||
FileExplorerTab::Local => {
|
||||
// Get selected file
|
||||
match self.local.files.get(self.local.index) {
|
||||
Some(entry) => Some(entry),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
FileExplorerTab::Remote => match self.remote.files.get(self.remote.index) {
|
||||
Some(entry) => Some(entry),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
// Get file_name and fill info list
|
||||
let file_name: String = match fsentry {
|
||||
Some(fsentry) => match fsentry {
|
||||
FsEntry::Directory(dir) => {
|
||||
// Push path
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Path: ", Style::default()),
|
||||
Span::styled(
|
||||
match &dir.symlink {
|
||||
Some(symlink) => {
|
||||
format!("{} => {}", dir.abs_path.display(), symlink.display())
|
||||
}
|
||||
None => dir.abs_path.to_string_lossy().to_string(),
|
||||
},
|
||||
Style::default()
|
||||
.fg(Color::LightYellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Push creation time
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Creation time: ", Style::default()),
|
||||
Span::styled(
|
||||
time_to_str(dir.creation_time, "%b %d %Y %H:%M:%S"),
|
||||
Style::default()
|
||||
.fg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Push Last change
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Last change time: ", Style::default()),
|
||||
Span::styled(
|
||||
time_to_str(dir.last_change_time, "%b %d %Y %H:%M:%S"),
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Push Last access
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Last access time: ", Style::default()),
|
||||
Span::styled(
|
||||
time_to_str(dir.last_access_time, "%b %d %Y %H:%M:%S"),
|
||||
Style::default()
|
||||
.fg(Color::LightMagenta)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// User
|
||||
#[cfg(any(unix, macos, linux))]
|
||||
let username: String = match dir.user {
|
||||
Some(uid) => match get_user_by_uid(uid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
None => uid.to_string(),
|
||||
},
|
||||
None => String::from("0"),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let username: String = format!("{}", dir.user.unwrap_or(0));
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("User: ", Style::default()),
|
||||
Span::styled(
|
||||
username,
|
||||
Style::default()
|
||||
.fg(Color::LightRed)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Group
|
||||
#[cfg(any(unix, macos, linux))]
|
||||
let group: String = match dir.group {
|
||||
Some(gid) => match get_group_by_gid(gid) {
|
||||
Some(group) => group.name().to_string_lossy().to_string(),
|
||||
None => gid.to_string(),
|
||||
},
|
||||
None => String::from("0"),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let group: String = format!("{}", dir.group.unwrap_or(0));
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Group: ", Style::default()),
|
||||
Span::styled(
|
||||
group,
|
||||
Style::default()
|
||||
.fg(Color::LightBlue)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Finally return file name
|
||||
dir.name.clone()
|
||||
}
|
||||
FsEntry::File(file) => {
|
||||
// Push path
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Path: ", Style::default()),
|
||||
Span::styled(
|
||||
match &file.symlink {
|
||||
Some(symlink) => {
|
||||
format!("{} => {}", file.abs_path.display(), symlink.display())
|
||||
}
|
||||
None => file.abs_path.to_string_lossy().to_string(),
|
||||
},
|
||||
Style::default()
|
||||
.fg(Color::LightYellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Push size
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Size: ", Style::default()),
|
||||
Span::styled(
|
||||
format!("{} ({})", ByteSize(file.size as u64), file.size),
|
||||
Style::default()
|
||||
.fg(Color::LightBlue)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Push creation time
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Creation time: ", Style::default()),
|
||||
Span::styled(
|
||||
time_to_str(file.creation_time, "%b %d %Y %H:%M:%S"),
|
||||
Style::default()
|
||||
.fg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Push Last change
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Last change time: ", Style::default()),
|
||||
Span::styled(
|
||||
time_to_str(file.last_change_time, "%b %d %Y %H:%M:%S"),
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Push Last access
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Last access time: ", Style::default()),
|
||||
Span::styled(
|
||||
time_to_str(file.last_access_time, "%b %d %Y %H:%M:%S"),
|
||||
Style::default()
|
||||
.fg(Color::LightMagenta)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// User
|
||||
#[cfg(any(unix, macos, linux))]
|
||||
let username: String = match file.user {
|
||||
Some(uid) => match get_user_by_uid(uid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
None => uid.to_string(),
|
||||
},
|
||||
None => String::from("0"),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let username: String = format!("{}", file.user.unwrap_or(0));
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("User: ", Style::default()),
|
||||
Span::styled(
|
||||
username,
|
||||
Style::default()
|
||||
.fg(Color::LightRed)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Group
|
||||
#[cfg(any(unix, macos, linux))]
|
||||
let group: String = match file.group {
|
||||
Some(gid) => match get_group_by_gid(gid) {
|
||||
Some(group) => group.name().to_string_lossy().to_string(),
|
||||
None => gid.to_string(),
|
||||
},
|
||||
None => String::from("0"),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let group: String = format!("{}", file.group.unwrap_or(0));
|
||||
info.push(ListItem::new(Spans::from(vec![
|
||||
Span::styled("Group: ", Style::default()),
|
||||
Span::styled(
|
||||
group,
|
||||
Style::default()
|
||||
.fg(Color::LightBlue)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])));
|
||||
// Finally return file name
|
||||
file.name.clone()
|
||||
}
|
||||
},
|
||||
None => String::from(""),
|
||||
};
|
||||
List::new(info)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default())
|
||||
.title(file_name),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
}
|
||||
|
||||
/// ### draw_footer
|
||||
///
|
||||
/// Draw authentication page footer
|
||||
@@ -518,62 +761,72 @@ impl FileTransferActivity {
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+D>",
|
||||
"<D>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw(" "),
|
||||
Span::raw("make directory"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+G>",
|
||||
"<G>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw(" "),
|
||||
Span::raw("goto path"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+H>",
|
||||
"<H>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw(" "),
|
||||
Span::raw("show help"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+Q>",
|
||||
"<I>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw(" "),
|
||||
Span::raw("show info about the selected file or directory"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<Q>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Quit TermSCP"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+R>",
|
||||
"<R>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw(" "),
|
||||
Span::raw("rename file"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+U>",
|
||||
"<U>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw(" "),
|
||||
Span::raw("go to parent directory"),
|
||||
])),
|
||||
];
|
||||
@@ -590,7 +843,7 @@ impl FileTransferActivity {
|
||||
/// align_text_center
|
||||
///
|
||||
/// Align text to center for a given width
|
||||
fn align_text_center(&self, text: &str, width: u16) -> String {
|
||||
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,
|
||||
@@ -601,4 +854,38 @@ impl FileTransferActivity {
|
||||
(0..indent_size).map(|_| " ").collect::<String>().as_str(),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### elide_wrkdir_path
|
||||
///
|
||||
/// Elide working directory path if longer than width + host.len
|
||||
/// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME}
|
||||
fn elide_wrkdir_path(wrkdir: &Path, host: &str, width: u16) -> PathBuf {
|
||||
let fmt_path: String = format!("{}", wrkdir.display());
|
||||
// NOTE: +5 is const
|
||||
match fmt_path.len() + host.len() + 5 > width as usize {
|
||||
false => PathBuf::from(wrkdir),
|
||||
true => {
|
||||
// Elide
|
||||
let ancestors_len: usize = wrkdir.ancestors().count();
|
||||
let mut ancestors = wrkdir.ancestors();
|
||||
let mut elided_path: PathBuf = PathBuf::new();
|
||||
// If ancestors_len's size is bigger than 2, push count - 2
|
||||
if ancestors_len > 2 {
|
||||
elided_path.push(ancestors.nth(ancestors_len - 2).unwrap());
|
||||
}
|
||||
// If ancestors_len is bigger than 3, push '...' and parent too
|
||||
if ancestors_len > 3 {
|
||||
elided_path.push("...");
|
||||
if let Some(parent) = wrkdir.ancestors().nth(1) {
|
||||
elided_path.push(parent.file_name().unwrap());
|
||||
}
|
||||
}
|
||||
// Push file_name
|
||||
if let Some(name) = wrkdir.file_name() {
|
||||
elided_path.push(name);
|
||||
}
|
||||
elided_path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,13 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Calculate progress percentage based on current progress
|
||||
pub(super) fn set_progress(&mut self, it: usize, sz: usize) {
|
||||
self.transfer_progress = ((it as f64) * 100.0) / (sz as f64);
|
||||
let mut prog: f64 = ((it as f64) * 100.0) / (sz as f64);
|
||||
// Check value
|
||||
if prog > 100.0 {
|
||||
prog = 100.0;
|
||||
} else if prog < 0.0 {
|
||||
prog = 0.0;
|
||||
}
|
||||
self.transfer_progress = prog;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ enum DialogYesNoOption {
|
||||
enum PopupType {
|
||||
Alert(Color, String), // Block color; Block text
|
||||
Fatal(String), // Must quit after being hidden
|
||||
FileInfo, // Show info about current file
|
||||
Help, // Show Help
|
||||
Input(String, OnInputSubmitCallback), // Input description; Callback for submit
|
||||
Progress(String), // Progress block text
|
||||
@@ -159,8 +160,8 @@ impl FileExplorer {
|
||||
/// Sort explorer files by their name
|
||||
pub fn sort_files_by_name(&mut self) {
|
||||
self.files.sort_by_key(|x: &FsEntry| match x {
|
||||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
FsEntry::Directory(dir) => dir.name.as_str().to_lowercase(),
|
||||
FsEntry::File(file) => file.name.as_str().to_lowercase(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,9 @@ impl FileTransferActivity {
|
||||
while buf_start < bytes_read {
|
||||
// Write bytes
|
||||
match rhnd.write(&buffer[buf_start..bytes_read]) {
|
||||
Ok(bytes) => buf_start += bytes,
|
||||
Ok(bytes) => {
|
||||
buf_start += bytes;
|
||||
}
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
@@ -204,6 +206,7 @@ impl FileTransferActivity {
|
||||
Color::Red,
|
||||
format!("Could not read local file: {}", err),
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Increase progress
|
||||
@@ -441,6 +444,7 @@ impl FileTransferActivity {
|
||||
err
|
||||
),
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,6 +460,7 @@ impl FileTransferActivity {
|
||||
Color::Red,
|
||||
format!("Could not read remote file: {}", err),
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Set progress
|
||||
|
||||
85
src/utils.rs
85
src/utils.rs
@@ -141,6 +141,59 @@ pub fn parse_remote_opt(
|
||||
Ok((address, port, protocol, username))
|
||||
}
|
||||
|
||||
/// ### fmt_pex
|
||||
///
|
||||
/// Convert 3 bytes of permissions value into ls notation (e.g. rwx-wx--x)
|
||||
pub fn fmt_pex(owner: u8, group: u8, others: u8) -> String {
|
||||
let mut mode: String = String::with_capacity(9);
|
||||
let read: u8 = (owner >> 2) & 0x1;
|
||||
let write: u8 = (owner >> 1) & 0x1;
|
||||
let exec: u8 = owner & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (group >> 2) & 0x1;
|
||||
let write: u8 = (group >> 1) & 0x1;
|
||||
let exec: u8 = group & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (others >> 2) & 0x1;
|
||||
let write: u8 = (others >> 1) & 0x1;
|
||||
let exec: u8 = others & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
mode
|
||||
}
|
||||
|
||||
/// ### instant_to_str
|
||||
///
|
||||
/// Format a `Instant` into a time string
|
||||
@@ -240,7 +293,7 @@ mod tests {
|
||||
assert_eq!(result.1, 21); // Fallback to ftp default
|
||||
assert_eq!(result.2, FileTransferProtocol::Ftp(false));
|
||||
assert!(result.3.is_none()); // Doesn't fall back
|
||||
// Protocol
|
||||
// Protocol
|
||||
let result: (String, u16, FileTransferProtocol, Option<String>) =
|
||||
parse_remote_opt(&String::from("scp://172.26.104.1"))
|
||||
.ok()
|
||||
@@ -249,7 +302,7 @@ mod tests {
|
||||
assert_eq!(result.1, 22); // Fallback to scp default
|
||||
assert_eq!(result.2, FileTransferProtocol::Scp);
|
||||
assert!(result.3.is_none()); // Doesn't fall back
|
||||
// Protocol + user
|
||||
// Protocol + user
|
||||
let result: (String, u16, FileTransferProtocol, Option<String>) =
|
||||
parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
|
||||
.ok()
|
||||
@@ -274,6 +327,18 @@ mod tests {
|
||||
assert!(parse_remote_opt(&String::from("172.26.104.1:abc")).is_err()); // Bad port
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_fmt_pex() {
|
||||
assert_eq!(fmt_pex(7, 7, 7), String::from("rwxrwxrwx"));
|
||||
assert_eq!(fmt_pex(7, 5, 5), String::from("rwxr-xr-x"));
|
||||
assert_eq!(fmt_pex(6, 6, 6), String::from("rw-rw-rw-"));
|
||||
assert_eq!(fmt_pex(6, 4, 4), String::from("rw-r--r--"));
|
||||
assert_eq!(fmt_pex(6, 0, 0), String::from("rw-------"));
|
||||
assert_eq!(fmt_pex(0, 0, 0), String::from("---------"));
|
||||
assert_eq!(fmt_pex(4, 4, 4), String::from("r--r--r--"));
|
||||
assert_eq!(fmt_pex(1, 2, 1), String::from("--x-w---x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_time_to_str() {
|
||||
let system_time: SystemTime = SystemTime::from(SystemTime::UNIX_EPOCH);
|
||||
@@ -290,28 +355,36 @@ mod tests {
|
||||
lstime_to_systime("Nov 5 16:32", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH).ok().unwrap(),
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1604593920)
|
||||
);
|
||||
assert_eq!(
|
||||
lstime_to_systime("Dec 2 21:32", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH).ok().unwrap(),
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1606944720)
|
||||
);
|
||||
assert_eq!(
|
||||
lstime_to_systime("Nov 5 2018", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH).ok().unwrap(),
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1541376000)
|
||||
);
|
||||
assert_eq!(
|
||||
lstime_to_systime("Mar 18 2018", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH).ok().unwrap(),
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1521331200)
|
||||
);
|
||||
// bad cases
|
||||
|
||||
Reference in New Issue
Block a user