20 Commits

Author SHA1 Message Date
veeso
87aa900bc6 0.3.2 2021-01-24 20:48:46 +01:00
veeso
e8e4cd22a1 0.3.2 ready 2021-01-24 20:31:24 +01:00
veeso
780cf592e4 Updated dependencies 2021-01-24 12:04:27 +01:00
Christian Visintin
45db58a0b3 Merge pull request #5 from veeso/ls-fmt
File explorer formatter
2021-01-24 11:59:31 +01:00
veeso
f5218bc582 test_utils_fmt_path_elide: don't run on windows 2021-01-24 11:48:00 +01:00
veeso
c5e2e02415 Optimized formatter: instead of replacing in fmt_str, keep 'prefix' in the call chain 2021-01-24 11:29:20 +01:00
veeso
e088772685 Docs 2021-01-23 16:38:46 +01:00
veeso
859daa3107 Clippy 2021-01-23 16:38:36 +01:00
veeso
56c580fc80 Use file_fmt in explorer activity 2021-01-23 16:26:25 +01:00
veeso
7a9ee697ff Added fmt_file to setupt activity 2021-01-23 16:22:32 +01:00
veeso
c16a2f6441 Improved getters/setters config client 2021-01-23 16:21:30 +01:00
veeso
b3c4385617 Added to ConfigClient getters/setters for file_fmt 2021-01-23 16:09:13 +01:00
veeso
e92370bd05 Added file_fmt to configuration 2021-01-23 16:00:41 +01:00
veeso
da0d5231bf Use formatter to fmt fs entries instead of fmt::Display trait 2021-01-23 15:51:46 +01:00
veeso
c1f6308795 Explorer formatter module 2021-01-23 15:41:07 +01:00
veeso
54ab24fc0c fmt_path_elide 2021-01-23 15:03:43 +01:00
veeso
d99efb9de4 SCP File transfer: when listing directory entries, check if a symlink points to a directory or to a file 2021-01-23 12:20:34 +01:00
ChristianVisintin
0c9ed38eb7 Solved file index in explorer files at start of termscp, in case the first entry is an hidden file 2021-01-19 09:13:08 +01:00
ChristianVisintin
ec801f1555 Working on 0.3.2 2021-01-19 09:12:59 +01:00
veeso
e0d8879961 typos 2021-01-18 19:09:54 +01:00
23 changed files with 1124 additions and 362 deletions

View File

@@ -1,6 +1,7 @@
# Changelog
- [Changelog](#changelog)
- [0.3.2](#032)
- [0.3.1](#031)
- [0.3.0](#030)
- [0.2.0](#020)
@@ -11,6 +12,25 @@
---
## 0.3.2
Released on 24/01/2021
- **Explorer Formatter**:
- Added possibility to customize the format when listing files in the explorers (Read more on README)
- Added `file_fmt` key to configuration (if missing, default will be used).
- Added the text input to the Settings view to set the value for `file_fmt`.
- Bugfix:
- Solved file index in explorer files at start of termscp, in case the first entry is an hidden file
- SCP File transfer: when listing directory entries, check if a symlink points to a directory or to a file
- Dependencies:
- updated `crossterm` to `0.19.0`
- updated `rand` to `0.8.2`
- updated `rpassword` to `5.0.1`
- updated `serde` to `1.0.121`
- updated `tui` to `0.14.0`
- updated `whoami` to `1.1.0`
## 0.3.1
Released on 18/01/2021

114
Cargo.lock generated
View File

@@ -128,9 +128,9 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
[[package]]
name = "bumpalo"
version = "3.4.0"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
checksum = "f07aa6688c702439a1be0307b6a94dffe1168569e45b9500c1372bc580740d59"
[[package]]
name = "byte-tools"
@@ -291,7 +291,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e86d73f2a0b407b5768d10a8c720cf5d2df49a9efc10ca09176d201ead4b7fb"
dependencies = [
"bitflags",
"crossterm_winapi",
"crossterm_winapi 0.6.2",
"lazy_static",
"libc",
"mio",
"parking_lot 0.11.1",
"signal-hook",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c"
dependencies = [
"bitflags",
"crossterm_winapi 0.7.0",
"lazy_static",
"libc",
"mio",
@@ -309,6 +325,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9"
dependencies = [
"winapi",
]
[[package]]
name = "crypto-mac"
version = "0.10.0"
@@ -470,9 +495,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4060f4657be78b8e766215b02b18a2e862d83745545de804638e2b545e81aee6"
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
dependencies = [
"cfg-if 1.0.0",
"libc",
@@ -598,9 +623,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.11"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2"
dependencies = [
"cfg-if 0.1.10",
]
@@ -772,6 +797,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
[[package]]
name = "opaque-debug"
version = "0.3.0"
@@ -841,7 +872,7 @@ dependencies = [
"cfg-if 0.1.10",
"cloudabi",
"libc",
"redox_syscall",
"redox_syscall 0.1.57",
"smallvec",
"winapi",
]
@@ -855,7 +886,7 @@ dependencies = [
"cfg-if 1.0.0",
"instant",
"libc",
"redox_syscall",
"redox_syscall 0.1.57",
"smallvec",
"winapi",
]
@@ -905,9 +936,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c24fcd450d3fa2b592732565aa4f17a27a61c65ece4726353e000939b0edee34"
checksum = "18519b42a40024d661e1714153e9ad0c3de27cd495760ceb09710920f1098b1e"
dependencies = [
"libc",
"rand_chacha 0.3.0",
@@ -950,7 +981,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5"
dependencies = [
"getrandom 0.2.1",
"getrandom 0.2.2",
]
[[package]]
@@ -977,6 +1008,15 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.3.5"
@@ -984,7 +1024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
dependencies = [
"getrandom 0.1.16",
"redox_syscall",
"redox_syscall 0.1.57",
"rust-argon2",
]
@@ -1017,9 +1057,9 @@ dependencies = [
[[package]]
name = "rpassword"
version = "5.0.0"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d755237fc0f99d98641540e66abac8bc46a0652f19148ac9e21de2da06b326c9"
checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb"
dependencies = [
"libc",
"winapi",
@@ -1117,18 +1157,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.118"
version = "1.0.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
checksum = "6159e3c76cab06f6bc466244d43b35e77e9500cd685da87620addadc2a4c40b1"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.118"
version = "1.0.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
checksum = "f3fcab8778dc651bc65cfab2e4eb64996f3c912b74002fb379c94517e1f27c46"
dependencies = [
"proc-macro2",
"quote",
@@ -1211,9 +1251,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
[[package]]
name = "syn"
version = "1.0.58"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5"
checksum = "07cb8b1b4ebf86a89ee88cbd201b022b94138c623644d035185c84d3f41b7e66"
dependencies = [
"proc-macro2",
"quote",
@@ -1222,27 +1262,27 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.1.0"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
"cfg-if 0.1.10",
"cfg-if 1.0.0",
"libc",
"rand 0.7.3",
"redox_syscall",
"rand 0.8.2",
"redox_syscall 0.2.4",
"remove_dir_all",
"winapi",
]
[[package]]
name = "termscp"
version = "0.3.1"
version = "0.3.2"
dependencies = [
"bitflags",
"bytesize",
"chrono",
"content_inspector",
"crossterm",
"crossterm 0.19.0",
"dirs",
"edit",
"ftp4",
@@ -1251,7 +1291,7 @@ dependencies = [
"keyring",
"lazy_static",
"magic-crypt",
"rand 0.8.1",
"rand 0.8.2",
"regex",
"rpassword",
"serde",
@@ -1277,11 +1317,11 @@ dependencies = [
[[package]]
name = "thread_local"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb9bc092d0d51e76b2b19d9d85534ffc9ec2db959a2523cdae0697e2972cd447"
checksum = "301bdd13d23c49672926be451130892d274d3ba0b410c18e00daa7990ff38d99"
dependencies = [
"lazy_static",
"once_cell",
]
[[package]]
@@ -1317,13 +1357,13 @@ dependencies = [
[[package]]
name = "tui"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4e6c82bb967df89f20b875fa8835fab5d5622c6a5efa574a1f0b6d0aa6e8f6"
checksum = "9ced152a8e9295a5b168adc254074525c17ac4a83c90b2716274cc38118bddc9"
dependencies = [
"bitflags",
"cassowary",
"crossterm",
"crossterm 0.18.2",
"unicode-segmentation",
"unicode-width",
]
@@ -1461,9 +1501,9 @@ dependencies = [
[[package]]
name = "whoami"
version = "1.0.3"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d595b2e146f36183d6a590b8d41568e2bc84c922267f43baf61c956330eeb436"
checksum = "1a921c0ad578a51c0b6c0bbb9b95f0ed11e90d61da506139e48a946edd11ee1e"
dependencies = [
"wasm-bindgen",
"web-sys",

View File

@@ -1,6 +1,6 @@
[package]
name = "termscp"
version = "0.3.1"
version = "0.3.2"
authors = ["Christian Visintin"]
edition = "2018"
license = "GPL-3.0"
@@ -20,7 +20,7 @@ bitflags = "1.2.1"
bytesize = "1.0.1"
chrono = "0.4.19"
content_inspector = "0.2.4"
crossterm = "0.18.2"
crossterm = "0.19.0"
dirs = "3.0.1"
edit = "0.1.2"
ftp4 = { version = "^4.0.2", features = ["secure"] }
@@ -28,17 +28,17 @@ getopts = "0.2.21"
hostname = "0.3.1"
lazy_static = "1.4.0"
magic-crypt = "3.1.6"
rand = "0.8.0"
rand = "0.8.2"
regex = "1.4.2"
rpassword = "5.0.0"
serde = { version = "1.0.118", features = ["derive"] }
rpassword = "5.0.1"
serde = { version = "1.0.121", features = ["derive"] }
ssh2 = "0.9.0"
tempfile = "3.1.0"
textwrap = "0.13.1"
toml = "0.5.8"
tui = { version = "0.13.0", features = ["crossterm"], default-features = false }
tui = { version = "0.14.0", features = ["crossterm"], default-features = false }
unicode-width = "0.1.7"
whoami = "1.0.1"
whoami = "1.1.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/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.3.1-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/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.3.2-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![Build](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions) [![codecov](https://codecov.io/gh/veeso/termscp/branch/main/graph/badge.svg?token=au67l7nQah)](https://codecov.io/gh/veeso/termscp)
~ Basically, WinSCP on a terminal ~
Developed by Christian Visintin
Current version: 0.3.1 (18/01/2021)
Current version: 0.3.2 (24/01/2021)
---
@@ -30,6 +30,7 @@ Current version: 0.3.1 (18/01/2021)
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
- [Configuration ⚙️](#configuration-)
- [SSH Key Storage 🔐](#ssh-key-storage-)
- [File Explorer Format](#file-explorer-format)
- [Keybindings ⌨](#keybindings-)
- [Documentation 📚](#documentation-)
- [Known issues 🧻](#known-issues-)
@@ -60,12 +61,16 @@ It happens quite often to me, when using SCP at work to forget the path of a fil
- SFTP
- SCP
- FTP and FTPS
- Compatible with Windows, Linux, BSD and MacOS
- Practical user interface to explore and operate on the remote and on the local machine file system
- Bookmarks and recent connections can be saved to access quickly to your favourite hosts
- Supports text editors to view and edit text files
- Supports both SFTP/SCP authentication through SSH keys and username/password
- User customization directly from the user interface
- Compatible with Windows, Linux, BSD and MacOS
- Customizations:
- Custom file explorer format
- Customizable text editor
- Customizable file sorting
- SSH key storage
- Written in Rust
- Easy to extend with new file transfers protocols
- Developed keeping an eye on performance
@@ -93,8 +98,8 @@ Requirements:
### Deb package 📦
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.1_amd64.deb)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.1_amd64.deb`
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.2_amd64.deb)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.2_amd64.deb`
then install through dpkg:
@@ -106,8 +111,8 @@ gdebi termscp_*.deb
### RPM package 📦
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.1-1.x86_64.rpm)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.1-1.x86_64.rpm`
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.2-1.x86_64.rpm)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.2-1.x86_64.rpm`
then install through rpm:
@@ -133,7 +138,7 @@ Start PowerShell as administrator and run
choco install termscp
```
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.3.1.nupkg)
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.3.2.nupkg)
and then with PowerShell started with administrator previleges, run:
@@ -235,9 +240,9 @@ If you go to [gallery](#gallery-), there is a GIF showing how bookmarks work
Well, kinda.
As said before, bookmarks are saved in your configuration directory along with passwords. Passwords are obviously not plain text, they are encrypted with **AES-128**. Does this make them safe? Well, depends on your operating system:
On Windows and MacOS the passwords are (if possible, but should be) in respectively the Windows Vault and the Keychain. This is actually super-safe and is directly managed by your operating system.
On Windows and MacOS the passwords are stored, if possible (but should be), in respectively the Windows Vault and the Keychain. This is actually super-safe and is directly managed by your operating system.
On Linux and BSD, on the other hand the key used to encrypt your passwords is stored on your drive. So it's still possible to retrieve the key to decrypt passwords. Luckily, the location of the key guarantees your key can't be read by users different from yours, but yeah, I still wouldn't save the password for a server exposed on the internet 😉.
On Linux and BSD, on the other hand, the key used to encrypt your passwords is stored on your drive (at $HOME/.config/termscp). It is then, still possible to retrieve the key to decrypt passwords. Luckily, the location of the key guarantees your key can't be read by users different from yours, but yeah, I still wouldn't save the password for a server exposed on the internet 😉.
Actually [keyring-rs](https://github.com/hwchen/keyring-rs), supports Linux, but for different reasons I preferred not to make it available for this configuration. If you want to read more about my decision read [this issue](https://github.com/veeso/termscp/issues/2), while if you think this might have been implemented differently feel free to open an issue with your proposal.
---
@@ -288,6 +293,25 @@ You can access the SSH key storage, from configuration moving to the `SSH Keys`
> Q: Wait, my private key is protected with password, can I use it?
> A: Of course you can. The password provided for authentication in termscp, is valid both for username/password authentication and for RSA key authentication.
### File Explorer Format
It is possible through configuration to define a custom format for the file explorer. This field, with name `File formatter syntax` will define how the files will be displayed in the file explorer.
The syntax for the formatter is the following `{KEY1}... {KEY2}... {KEYn}...`.
Each key in bracket will be replaced with the related attribute, while everything outside brackets will be left unchanged.
These are the keys supported by the formatter:
- `ATIME`: Last access time (with syntax `%b %d %Y %H:%M`)
- `CTIME`: Creation time (with syntax `%b %d %Y %H:%M`)
- `GROUP`: Owner group
- `MTIME`: Last change time (with syntax `%b %d %Y %H:%M`)
- `NAME`: File name (Elided if longer than 24)
- `PEX`: File permissions (UNIX format)
- `SIZE`: File size (omitted for directories)
- `SYMLINK`: Symlink (if any `-> {FILE_PATH}`)
- `USER`: Owner user
If left empty, the default formatter syntax will be used: `{NAME} {PEX} {USER} {SIZE} {MTIME}`
---
## Keybindings ⌨

View File

@@ -1,14 +1,14 @@
pkgbase = termscp-bin
pkgdesc = TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal.
pkgver = 0.3.1
pkgver = 0.3.2
pkgrel = 1
url = https://github.com/veeso/termscp
arch = x86_64
license = GPL-3.0
provides = termscp
options = strip
source = https://github.com/veeso/termscp/releases/download/v0.3.1/termscp-0.3.1-x86_64.tar.gz
sha256sums = dd056531554737595cbe5ac9ff741fdabf5e386299bbc0c81ea9e0f00fbbe2d0
source = https://github.com/veeso/termscp/releases/download/v0.3.2/termscp-0.3.2-x86_64.tar.gz
sha256sums = e2700e2e9b741eb273e2633d5cf24ad620365d059bdd4f2b42f3737a7c28a2c7
pkgname = termscp-bin

View File

@@ -1,6 +1,6 @@
# Maintainer: Christian Visintin
pkgname=termscp
pkgver=0.3.1
pkgver=0.3.2
pkgrel=1
pkgdesc="TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal."
url="https://github.com/veeso/termscp"
@@ -9,7 +9,7 @@ arch=("x86_64")
provides=("termscp")
options=("strip")
source=("https://github.com/veeso/termscp/releases/download/v$pkgver/termscp-$pkgver-x86_64.tar.gz")
sha256sums=("dd056531554737595cbe5ac9ff741fdabf5e386299bbc0c81ea9e0f00fbbe2d0")
sha256sums=("e2700e2e9b741eb273e2633d5cf24ad620365d059bdd4f2b42f3737a7c28a2c7")
package() {
install -Dm755 termscp -t "$pkgdir/usr/bin/"

View File

@@ -56,6 +56,7 @@ pub struct UserInterfaceConfig {
pub default_protocol: String,
pub show_hidden_files: bool,
pub group_dirs: Option<String>,
pub file_fmt: Option<String>,
}
#[derive(Deserialize, Serialize, std::fmt::Debug)]
@@ -85,6 +86,7 @@ impl Default for UserInterfaceConfig {
default_protocol: FileTransferProtocol::Sftp.to_string(),
show_hidden_files: false,
group_dirs: None,
file_fmt: None,
}
}
}
@@ -171,6 +173,7 @@ mod tests {
text_editor: PathBuf::from("nano"),
show_hidden_files: true,
group_dirs: Some(String::from("first")),
file_fmt: Some(String::from("{NAME}")),
};
let cfg: UserConfig = UserConfig {
user_interface: ui,
@@ -187,6 +190,7 @@ mod tests {
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
}
#[test]

View File

@@ -108,6 +108,7 @@ mod tests {
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME} {PEX}")));
// Verify keys
assert_eq!(
*cfg.remote
@@ -143,6 +144,7 @@ mod tests {
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.group_dirs, None);
assert_eq!(cfg.user_interface.file_fmt, None);
// Verify keys
assert_eq!(
*cfg.remote
@@ -199,6 +201,7 @@ mod tests {
text_editor = "vim"
show_hidden_files = true
group_dirs = "last"
file_fmt = "{NAME} {PEX}"
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"

View File

@@ -83,7 +83,8 @@ impl ScpFileTransfer {
}
// Collect metadata
// Get if is directory and if is symlink
let (is_dir, is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str() {
let (mut is_dir, is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str()
{
"-" => (false, false),
"l" => (false, true),
"d" => (true, false),
@@ -135,12 +136,21 @@ impl ScpFileTransfer {
Err(_) => None,
};
// Get filesize
let filesize: usize = metadata.get(6).unwrap().as_str().parse::<usize>().unwrap_or(0);
let filesize: usize = metadata
.get(6)
.unwrap()
.as_str()
.parse::<usize>()
.unwrap_or(0);
// Get link and name
let (file_name, symlink_path): (String, Option<PathBuf>) = match is_symlink {
true => self.get_name_and_link(metadata.get(8).unwrap().as_str()),
false => (String::from(metadata.get(8).unwrap().as_str()), None),
};
// Check if symlink points to a directory
if let Some(symlink_path) = symlink_path.as_ref() {
is_dir = symlink_path.is_dir();
}
// Get symlink
let symlink: Option<Box<FsEntry>> = match symlink_path {
None => None,

View File

@@ -24,6 +24,7 @@
*/
// Locals
use super::formatter::Formatter;
use super::{ExplorerOpts, FileExplorer, FileSorting, GroupDirs};
// Ext
use std::collections::VecDeque;
@@ -95,6 +96,18 @@ impl FileExplorerBuilder {
}
self
}
/// ### with_formatter
///
/// Set formatter for FileExplorer
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
if let Some(fmt_str) = fmt_str {
e.fmt = Formatter::new(fmt_str);
}
}
self
}
}
#[cfg(test)]
@@ -119,6 +132,7 @@ mod tests {
.with_group_dirs(Some(GroupDirs::First))
.with_hidden_files(true)
.with_stack_size(24)
.with_formatter(Some("{NAME}"))
.build();
// Verify
assert!(explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES));

View File

@@ -0,0 +1,718 @@
//! ## Formatter
//!
//! `formatter` is the module which provides formatting utilities for `FileExplorer`
/*
*
* Copyright (C) 2020-2021 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/>.
*
*/
// Deps
extern crate bytesize;
extern crate regex;
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
extern crate users;
// Locals
use super::FsEntry;
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
// Ext
use bytesize::ByteSize;
use regex::Regex;
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
use users::{get_group_by_gid, get_user_by_uid};
// Types
type FmtCallback = fn(&Formatter, &FsEntry, &str, &str) -> String;
// Keys
const FMT_KEY_ATIME: &str = "{ATIME}";
const FMT_KEY_CTIME: &str = "{CTIME}";
const FMT_KEY_GROUP: &str = "{GROUP}";
const FMT_KEY_MTIME: &str = "{MTIME}";
const FMT_KEY_NAME: &str = "{NAME}";
const FMT_KEY_PEX: &str = "{PEX}";
const FMT_KEY_SIZE: &str = "{SIZE}";
const FMT_KEY_SYMLINK: &str = "{SYMLINK}";
const FMT_KEY_USER: &str = "{USER}";
// Default
const FMT_DEFAULT_STX: &str = "{NAME} {PEX} {USER} {SIZE} {MTIME}";
// Regex
lazy_static! {
static ref FMT_KEY_REGEX: Regex = Regex::new(r"\{(.*?)\}").ok().unwrap();
}
/// ## CallChainBlock
///
/// Call Chain block is a block in a chain of functions which are called in order to format the FsEntry.
/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found
/// a chain of function is made using the Formatters method.
/// This method provides an extremely fast way to format fs entries
struct CallChainBlock {
func: FmtCallback,
prefix: String,
next_block: Option<Box<CallChainBlock>>,
}
impl CallChainBlock {
/// ### new
///
/// Create a new `CallChainBlock`
pub fn new(func: FmtCallback, prefix: String) -> Self {
CallChainBlock {
func,
prefix,
next_block: None,
}
}
/// ### next
///
/// Call next callback in the CallChain
pub fn next(&self, fmt: &Formatter, fsentry: &FsEntry, cur_str: &str) -> String {
// Call func
let new_str: String = (self.func)(fmt, fsentry, cur_str, self.prefix.as_str());
// If next is some, call next, otherwise (END OF CHAIN) return new_str
match &self.next_block {
Some(block) => block.next(fmt, fsentry, new_str.as_str()),
None => new_str,
}
}
/// ### push
///
/// Push func to the last element in the Call chain
pub fn push(&mut self, func: FmtCallback, prefix: String) {
// Call recursively until an element with next_block equal to None is found
match &mut self.next_block {
None => self.next_block = Some(Box::new(CallChainBlock::new(func, prefix))),
Some(block) => block.push(func, prefix),
}
}
}
/// ## Formatter
///
/// Formatter takes care of formatting FsEntries according to the provided keys.
/// Formatting is performed using the `CallChainBlock`, which composed makes a Call Chain. This method is extremely fast compared to match the format groups
/// at each fmt call.
pub struct Formatter {
call_chain: CallChainBlock,
}
impl Default for Formatter {
/// ### default
///
/// Instantiates a Formatter with the default fmt syntax
fn default() -> Self {
Formatter {
call_chain: Self::make_callchain(FMT_DEFAULT_STX),
}
}
}
impl Formatter {
/// ### new
///
/// Instantiates a new `Formatter` with the provided format string
pub fn new(fmt_str: &str) -> Self {
Formatter {
call_chain: Self::make_callchain(fmt_str),
}
}
/// ### fmt
///
/// Format fsentry
pub fn fmt(&self, fsentry: &FsEntry) -> String {
// Execute callchain blocks
self.call_chain.next(self, fsentry, "")
}
// Fmt methods
/// ### fmt_atime
///
/// Format last access time
fn fmt_atime(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
// Get date
let datetime: String = fmt_time(fsentry.get_last_access_time(), "%b %d %Y %H:%M");
// Add to cur str, prefix and the key value
format!("{}{}{:17}", cur_str, prefix, datetime)
}
/// ### fmt_ctime
///
/// Format creation time
fn fmt_ctime(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
// Get date
let datetime: String = fmt_time(fsentry.get_creation_time(), "%b %d %Y %H:%M");
// Add to cur str, prefix and the key value
format!("{}{}{:17}", cur_str, prefix, datetime)
}
/// ### fmt_group
///
/// Format owner group
fn fmt_group(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
// Get username
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
let group: String = match fsentry.get_group() {
Some(gid) => match get_group_by_gid(gid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => gid.to_string(),
},
None => 0.to_string(),
};
#[cfg(target_os = "windows")]
let group: String = match fsentry.get_group() {
Some(gid) => gid.to_string(),
None => 0.to_string(),
};
// Add to cur str, prefix and the key value
format!("{}{}{:12}", cur_str, prefix, group)
}
/// ### fmt_mtime
///
/// Format last change time
fn fmt_mtime(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
// Get date
let datetime: String = fmt_time(fsentry.get_last_change_time(), "%b %d %Y %H:%M");
// Add to cur str, prefix and the key value
format!("{}{}{:17}", cur_str, prefix, datetime)
}
/// ### fmt_name
///
/// Format file name
fn fmt_name(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
// Get file name (or elide if too long)
let name: &str = fsentry.get_name();
let last_idx: usize = match fsentry.is_dir() {
// NOTE: For directories is 19, since we push '/' to name
true => 19,
false => 20,
};
let mut name: String = match name.len() >= 24 {
false => name.to_string(),
true => format!("{}...", &name[0..last_idx]),
};
if fsentry.is_dir() {
name.push('/');
}
// Add to cur str, prefix and the key value
format!("{}{}{:24}", cur_str, prefix, name)
}
/// ### fmt_pex
///
/// Format file permissions
fn fmt_pex(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
// Create mode string
let mut pex: String = String::with_capacity(10);
let file_type: char = match fsentry.is_symlink() {
true => 'l',
false => match fsentry.is_dir() {
true => 'd',
false => '-',
},
};
pex.push(file_type);
match fsentry.get_unix_pex() {
None => pex.push_str("?????????"),
Some((owner, group, others)) => pex.push_str(fmt_pex(owner, group, others).as_str()),
}
// Add to cur str, prefix and the key value
format!("{}{}{:10}", cur_str, prefix, pex)
}
/// ### fmt_size
///
/// Format file size
fn fmt_size(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
if fsentry.is_file() {
// Get byte size
let size: ByteSize = ByteSize(fsentry.get_size() as u64);
// Add to cur str, prefix and the key value
format!("{}{}{:10}", cur_str, prefix, size.to_string())
} else {
// Add to cur str, prefix and the key value
format!("{}{} ", cur_str, prefix)
}
}
/// ### fmt_symlink
///
/// Format file symlink (if any)
fn fmt_symlink(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
// Get file name (or elide if too long)
// Replace `FMT_KEY_NAME` with name
match fsentry.is_symlink() {
false => format!("{}{} ", cur_str, prefix),
true => format!(
"{}{}-> {:21}",
cur_str,
prefix,
fmt_path_elide(fsentry.get_realfile().get_abs_path().as_path(), 20)
),
}
}
/// ### fmt_user
///
/// Format owner user
fn fmt_user(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
// Get username
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
let username: String = match fsentry.get_user() {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
},
None => 0.to_string(),
};
#[cfg(target_os = "windows")]
let username: String = match fsentry.get_user() {
Some(uid) => uid.to_string(),
None => 0.to_string(),
};
// Add to cur str, prefix and the key value
format!("{}{}{:12}", cur_str, prefix, username)
}
/// ### fmt_fallback
///
/// Fallback function in case the format key is unknown
/// It does nothing, just returns cur_str
fn fmt_fallback(&self, _fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
// Add to cur str and prefix
format!("{}{}", cur_str, prefix)
}
// Static
/// ### make_callchain
///
/// Make a callchain starting from the fmt str
fn make_callchain(fmt_str: &str) -> CallChainBlock {
// Init chain block
let mut callchain: Option<CallChainBlock> = None;
// Track index of the last match found, to get the prefix for each token
let mut last_index: usize = 0;
// Match fmt str against regex
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
// Get match index (unwrap is safe, since always exists)
let index: usize = fmt_str.find(&regex_match[0]).unwrap();
// Get prefix
let prefix: String = String::from(&fmt_str[last_index..index]);
// Increment last index (sum prefix lenght and the length of the key)
last_index += prefix.len() + regex_match[0].len();
// Match the match (I guess...)
let callback: FmtCallback = match &regex_match[0] {
FMT_KEY_ATIME => Self::fmt_atime,
FMT_KEY_CTIME => Self::fmt_ctime,
FMT_KEY_GROUP => Self::fmt_group,
FMT_KEY_MTIME => Self::fmt_mtime,
FMT_KEY_NAME => Self::fmt_name,
FMT_KEY_PEX => Self::fmt_pex,
FMT_KEY_SIZE => Self::fmt_size,
FMT_KEY_SYMLINK => Self::fmt_symlink,
FMT_KEY_USER => Self::fmt_user,
_ => Self::fmt_fallback,
};
// Create a callchain or push new element to its back
match callchain.as_mut() {
None => callchain = Some(CallChainBlock::new(callback, prefix)),
Some(chain_block) => chain_block.push(callback, prefix),
}
}
// Finalize and return
match callchain {
Some(callchain) => callchain,
None => CallChainBlock::new(Self::fmt_fallback, String::new()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::{FsDirectory, FsFile};
use std::path::PathBuf;
use std::time::SystemTime;
#[test]
fn test_fs_explorer_formatter_callchain() {
// Make a dummy formatter
let dummy_formatter: Formatter = Formatter::new("");
// Make a dummy entry
let t_now: SystemTime = SystemTime::now();
let dummy_entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
let prefix: String = String::from("h");
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix);
assert!(callchain.next_block.is_none());
assert_eq!(callchain.prefix, String::from("h"));
// Execute
assert_eq!(
callchain.next(&dummy_formatter, &dummy_entry, ""),
String::from("hA")
);
// Push 4 new blocks
callchain.push(dummy_fmt, String::from("h"));
callchain.push(dummy_fmt, String::from("h"));
callchain.push(dummy_fmt, String::from("h"));
callchain.push(dummy_fmt, String::from("h"));
// Verify
assert_eq!(
callchain.next(&dummy_formatter, &dummy_entry, ""),
String::from("hAhAhAhAhA")
);
}
#[test]
fn test_fs_explorer_formatter_format_files() {
// Make default
let formatter: Formatter = Formatter::default();
// Experiments :D
let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -rw-r--r-- root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -rw-r--r-- 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// Elide name
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("piroparoporoperoperupupu.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
formatter.fmt(&entry),
format!(
"piroparoporoperoperu... -rw-r--r-- root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"piroparoporoperoperu... -rw-r--r-- 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// No pex
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// No user
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_formatter_format_dirs() {
// Make default
let formatter: Formatter = Formatter::default();
// Experiments :D
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
readonly: false,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((7, 5, 5)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ drwxr-xr-x root {}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ drwxr-xr-x 0 {}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
// No pex, no user
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
readonly: false,
symlink: None, // UNIX only
user: None, // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ d????????? 0 {}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ d????????? 0 {}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_formatter_all_together_now() {
let formatter: Formatter =
Formatter::new("{NAME} {SYMLINK} {GROUP} {USER} {PEX} {SIZE} {ATIME} {CTIME} {MTIME}");
// Directory (with symlink)
let t: SystemTime = SystemTime::now();
let pointer: FsEntry = FsEntry::File(FsFile {
name: String::from("project.info"),
abs_path: PathBuf::from("/project.info"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: None, // UNIX only
});
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/project"),
last_change_time: t,
last_access_time: t,
creation_time: t,
readonly: false,
symlink: Some(Box::new(pointer)), // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((7, 5, 5)), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"projects/ -> /project.info 0 0 lrwxr-xr-x {} {} {}",
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
));
// Directory without symlink
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/project"),
last_change_time: t,
last_access_time: t,
creation_time: t,
readonly: false,
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((7, 5, 5)), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"projects/ 0 0 drwxr-xr-x {} {} {}",
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
));
// File with symlink
let pointer: FsEntry = FsEntry::File(FsFile {
name: String::from("project.info"),
abs_path: PathBuf::from("/project.info"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: None, // UNIX only
});
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: Some(Box::new(pointer)), // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt -> /project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
));
// File without symlink
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
));
}
/// ### dummy_fmt
///
/// Dummy formatter, just yelds an 'A' at the end of the current string
fn dummy_fmt(_fmt: &Formatter, _entry: &FsEntry, cur_str: &str, prefix: &str) -> String {
format!("{}{}A", cur_str, prefix)
}
}

View File

@@ -25,10 +25,12 @@
// Mods
pub(crate) mod builder;
mod formatter;
// Deps
extern crate bitflags;
// Locals
use super::FsEntry;
use formatter::Formatter;
// Ext
use std::cmp::Reverse;
use std::collections::VecDeque;
@@ -75,6 +77,7 @@ pub struct FileExplorer {
pub(crate) file_sorting: FileSorting, // File sorting criteria
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
pub(crate) opts: ExplorerOpts, // Explorer options
pub(crate) fmt: Formatter, // FsEntry formatter
index: usize, // Selected file
files: Vec<FsEntry>, // Files in directory
}
@@ -88,6 +91,7 @@ impl Default for FileExplorer {
file_sorting: FileSorting::ByName,
group_dirs: None,
opts: ExplorerOpts::empty(),
fmt: Formatter::default(),
index: 0,
files: Vec::new(),
}
@@ -166,6 +170,15 @@ impl FileExplorer {
self.files.get(self.index)
}
// Formatting
/// ### fmt_file
///
/// Format a file entry
pub fn fmt_file(&self, entry: &FsEntry) -> String {
self.fmt.fmt(entry)
}
// Sorting
/// ### sort_by
@@ -239,7 +252,8 @@ impl FileExplorer {
///
/// Sort files by creation time; the newest comes first
fn sort_files_by_creation_time(&mut self) {
self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time()));
self.files
.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time()));
}
/// ### sort_files_by_size
@@ -507,6 +521,7 @@ mod tests {
use super::*;
use crate::fs::{FsDirectory, FsFile};
use crate::utils::fmt::fmt_time;
use std::thread::sleep;
use std::time::{Duration, SystemTime};
@@ -854,6 +869,43 @@ mod tests {
assert_eq!(explorer.files.get(7).unwrap().get_name(), "README.md");
}
#[test]
fn test_fs_explorer_fmt() {
let explorer: FileExplorer = FileExplorer::default();
// Create fs entry
let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
explorer.fmt_file(&entry),
format!(
"bar.txt -rw-r--r-- root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
explorer.fmt_file(&entry),
format!(
"bar.txt -rw-r--r-- 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_to_string_from_str_traits() {
// File Sorting

View File

@@ -25,19 +25,9 @@
// Mod
pub mod explorer;
// Deps
extern crate bytesize;
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
extern crate users;
// Locals
use crate::utils::fmt::{fmt_pex, fmt_time};
// Ext
use bytesize::ByteSize;
use std::path::PathBuf;
use std::time::SystemTime;
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
use users::get_user_by_uid;
/// ## FsEntry
///
@@ -236,76 +226,6 @@ impl FsEntry {
}
}
impl std::fmt::Display for FsEntry {
/// ### fmt_ls
///
/// Format File Entry as `ls` does
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// Create mode string
let mut mode: String = String::with_capacity(10);
let file_type: char = match self.is_symlink() {
true => 'l',
false => match self.is_dir() {
true => 'd',
false => '-',
},
};
mode.push(file_type);
match self.get_unix_pex() {
None => mode.push_str("?????????"),
Some((owner, group, others)) => mode.push_str(fmt_pex(owner, group, others).as_str()),
}
// Get username
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
let username: String = match self.get_user() {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
},
None => 0.to_string(),
};
#[cfg(target_os = "windows")]
let username: String = match self.get_user() {
Some(uid) => uid.to_string(),
None => 0.to_string(),
};
// Get group
/*
let group: String = match self.get_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"),
};
*/
// Get byte size
let size: ByteSize = ByteSize(self.get_size() as u64);
// Get date
let datetime: String = fmt_time(self.get_last_change_time(), "%b %d %Y %H:%M");
// Set file name (or elide if too long)
let name: &str = self.get_name();
let last_idx: usize = match self.is_dir() {
// NOTE: For directories is 19, since we push '/' to name
true => 19,
false => 20,
};
let mut name: String = match name.len() >= 24 {
false => name.to_string(),
true => format!("{}...", &name[0..last_idx]),
};
// If is directory, append '/'
if self.is_dir() {
name.push('/');
}
write!(
f,
"{:24}\t{:12}\t{:12}\t{:10}\t{:17}",
name, mode, username, size, datetime
)
}
}
#[cfg(test)]
mod tests {
@@ -512,194 +432,4 @@ mod tests {
PathBuf::from("/home/cvisintin/projects")
);
}
#[test]
fn test_fs_fmt_file() {
let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
format!("{}", entry),
format!(
"bar.txt \t-rw-r--r-- \troot \t8.2 KB \t{}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
format!("{}", entry),
format!(
"bar.txt \t-rw-r--r-- \t0 \t8.2 KB \t{}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// Elide name
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("piroparoporoperoperupupu.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
format!("{}", entry),
format!(
"piroparoporoperoperu... \t-rw-r--r-- \troot \t8.2 KB \t{}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
format!("{}", entry),
format!(
"piroparoporoperoperu... \t-rw-r--r-- \t0 \t8.2 KB \t{}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// No pex
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
format!("{}", entry),
format!(
"bar.txt \t-????????? \troot \t8.2 KB \t{}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
format!("{}", entry),
format!(
"bar.txt \t-????????? \t0 \t8.2 KB \t{}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// No user
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
format!("{}", entry),
format!(
"bar.txt \t-????????? \t0 \t8.2 KB \t{}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
format!("{}", entry),
format!(
"bar.txt \t-????????? \t0 \t8.2 KB \t{}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_fmt_dir() {
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
readonly: false,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((7, 5, 5)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
format!("{}", entry),
format!(
"projects/ \tdrwxr-xr-x \troot \t4.1 KB \t{}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
format!("{}", entry),
format!(
"projects/ \tdrwxr-xr-x \t0 \t4.1 KB \t{}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
// No pex, no user
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
readonly: false,
symlink: None, // UNIX only
user: None, // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
format!("{}", entry),
format!(
"projects/ \td????????? \t0 \t4.1 KB \t{}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
format!("{}", entry),
format!(
"projects/ \td????????? \t0 \t4.1 KB \t{}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
}
}

View File

@@ -163,6 +163,23 @@ impl ConfigClient {
};
}
/// ### get_file_fmt
///
/// Get current file fmt
pub fn get_file_fmt(&self) -> Option<String> {
self.config.user_interface.file_fmt.clone()
}
/// ### set_file_fmt
///
/// Set file fmt parameter
pub fn set_file_fmt(&mut self, s: String) {
self.config.user_interface.file_fmt = match s.is_empty() {
true => None,
false => Some(s),
};
}
// SSH Keys
/// ### save_ssh_key
@@ -451,6 +468,21 @@ mod tests {
assert_eq!(client.get_group_dirs(), None,);
}
#[test]
fn test_system_config_file_fmt() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
assert_eq!(client.get_file_fmt(), None);
client.set_file_fmt(String::from("{NAME}"));
assert_eq!(client.get_file_fmt().unwrap(), String::from("{NAME}"));
// Delete
client.set_file_fmt(String::from(""));
assert_eq!(client.get_file_fmt(), None);
}
#[test]
fn test_system_config_ssh_keys() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();

View File

@@ -169,7 +169,7 @@ impl FileTransferActivity {
let files: Vec<ListItem> = self
.local
.iter_files()
.map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
.map(|entry: &FsEntry| ListItem::new(Span::from(self.local.fmt_file(entry))))
.collect();
// Get colors to use; highlight element inverting fg/bg only when tab is active
let (fg, bg): (Color, Color) = match self.tab {
@@ -209,7 +209,7 @@ impl FileTransferActivity {
let files: Vec<ListItem> = self
.remote
.iter_files()
.map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
.map(|entry: &FsEntry| ListItem::new(Span::from(self.remote.fmt_file(entry))))
.collect();
// Get colors to use; highlight element inverting fg/bg only when tab is active
let (fg, bg): (Color, Color) = match self.tab {

View File

@@ -133,6 +133,7 @@ impl FileTransferActivity {
.with_group_dirs(cli.get_group_dirs())
.with_hidden_files(cli.get_show_hidden_files())
.with_stack_size(16)
.with_formatter(cli.get_file_fmt().as_deref())
.build(),
None => FileExplorerBuilder::new() // Build default
.with_file_sorting(FileSorting::ByName)

View File

@@ -303,6 +303,8 @@ impl Activity for FileTransferActivity {
// Get files at current wd
self.local_scan(pwd.as_path());
self.local.wrkdir = pwd;
// Index at first valid
self.local.index_at_first();
// Configure text editor
self.setup_text_editor();
}

View File

@@ -108,6 +108,8 @@ impl FileTransferActivity {
// Get current entries
if let Ok(pwd) = self.client.pwd() {
self.remote_scan(pwd.as_path());
// Set index at first valid
self.remote.index_at_first();
// Set wrkdir
self.remote.wrkdir = pwd;
}

View File

@@ -181,16 +181,28 @@ impl SetupActivity {
KeyCode::Backspace => {
// Pop character from selected input
if let Some(config_cli) = self.config_cli.as_mut() {
// NOTE: replace with match if other text fields are added
if matches!(field, UserInterfaceInputField::TextEditor) {
// Pop from text editor
let mut input: String = String::from(
config_cli.get_text_editor().as_path().to_string_lossy(),
);
input.pop();
// Update text editor value
config_cli.set_text_editor(PathBuf::from(input.as_str()));
match field {
UserInterfaceInputField::TextEditor => {
// Pop from text editor
let mut input: String = String::from(
config_cli.get_text_editor().as_path().to_string_lossy(),
);
input.pop();
// Update text editor value
config_cli.set_text_editor(PathBuf::from(input.as_str()));
}
UserInterfaceInputField::FileFmt => {
// Push char to current file fmt
let mut file_fmt = config_cli.get_file_fmt().unwrap_or_default();
// Pop from file fmt
file_fmt.pop();
// If len is 0, will become None
config_cli.set_file_fmt(file_fmt);
}
_ => { /* Not a text field */ }
}
// NOTE: replace with match if other text fields are added
if matches!(field, UserInterfaceInputField::TextEditor) {}
}
}
KeyCode::Left => {
@@ -270,6 +282,7 @@ impl SetupActivity {
KeyCode::Up => {
// Change selected field
self.tab = SetupTab::UserInterface(match field {
UserInterfaceInputField::FileFmt => UserInterfaceInputField::GroupDirs,
UserInterfaceInputField::GroupDirs => {
UserInterfaceInputField::ShowHiddenFiles
}
@@ -279,7 +292,7 @@ impl SetupActivity {
UserInterfaceInputField::DefaultProtocol => {
UserInterfaceInputField::TextEditor
}
UserInterfaceInputField::TextEditor => UserInterfaceInputField::GroupDirs, // Wrap
UserInterfaceInputField::TextEditor => UserInterfaceInputField::FileFmt, // Wrap
});
}
KeyCode::Down => {
@@ -294,7 +307,8 @@ impl SetupActivity {
UserInterfaceInputField::ShowHiddenFiles => {
UserInterfaceInputField::GroupDirs
}
UserInterfaceInputField::GroupDirs => UserInterfaceInputField::TextEditor, // Wrap
UserInterfaceInputField::GroupDirs => UserInterfaceInputField::FileFmt,
UserInterfaceInputField::FileFmt => UserInterfaceInputField::TextEditor, // Wrap
});
}
KeyCode::Char(ch) => {
@@ -328,14 +342,24 @@ impl SetupActivity {
// Push character to input field
if let Some(config_cli) = self.config_cli.as_mut() {
// NOTE: change to match if other fields are added
if matches!(field, UserInterfaceInputField::TextEditor) {
// Get current text editor and push character
let mut input: String = String::from(
config_cli.get_text_editor().as_path().to_string_lossy(),
);
input.push(ch);
// Update text editor value
config_cli.set_text_editor(PathBuf::from(input.as_str()));
match field {
UserInterfaceInputField::TextEditor => {
// Get current text editor and push character
let mut input: String = String::from(
config_cli.get_text_editor().as_path().to_string_lossy(),
);
input.push(ch);
// Update text editor value
config_cli.set_text_editor(PathBuf::from(input.as_str()));
}
UserInterfaceInputField::FileFmt => {
// Push char to current file fmt
let mut file_fmt = config_cli.get_file_fmt().unwrap_or_default();
file_fmt.push(ch);
// update value
config_cli.set_file_fmt(file_fmt);
}
_ => { /* Not a text field */ }
}
}
}

View File

@@ -89,6 +89,7 @@ impl SetupActivity {
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(1),
]
.as_ref(),
@@ -107,15 +108,28 @@ impl SetupActivity {
if let Some(tab) = self.draw_default_group_dirs_tab() {
f.render_widget(tab, ui_cfg_chunks[3]);
}
if let Some(tab) = self.draw_file_fmt_input() {
f.render_widget(tab, ui_cfg_chunks[4]);
}
// Set cursor
if let Some(cli) = &self.config_cli {
if matches!(form_field, UserInterfaceInputField::TextEditor) {
let editor_text: String =
String::from(cli.get_text_editor().as_path().to_string_lossy());
f.set_cursor(
ui_cfg_chunks[0].x + editor_text.width() as u16 + 1,
ui_cfg_chunks[0].y + 1,
)
match form_field {
UserInterfaceInputField::TextEditor => {
let editor_text: String =
String::from(cli.get_text_editor().as_path().to_string_lossy());
f.set_cursor(
ui_cfg_chunks[0].x + editor_text.width() as u16 + 1,
ui_cfg_chunks[0].y + 1,
);
}
UserInterfaceInputField::FileFmt => {
let file_fmt: String = cli.get_file_fmt().unwrap_or_default();
f.set_cursor(
ui_cfg_chunks[4].x + file_fmt.width() as u16 + 1,
ui_cfg_chunks[4].y + 1,
);
}
_ => { /* Not a text field */ }
}
}
}
@@ -392,6 +406,31 @@ impl SetupActivity {
}
}
/// ### draw_file_fmt_input
///
/// Draw input text field for file fmt
fn draw_file_fmt_input(&self) -> Option<Paragraph> {
match &self.config_cli {
Some(cli) => Some(
Paragraph::new(cli.get_file_fmt().unwrap_or_default())
.style(Style::default().fg(match &self.tab {
SetupTab::SshConfig => Color::White,
SetupTab::UserInterface(field) => match field {
UserInterfaceInputField::FileFmt => Color::LightCyan,
_ => Color::White,
},
}))
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title("File formatter syntax"),
),
),
None => None,
}
}
/// ### draw_ssh_keys_list
///
/// Draw ssh keys list

View File

@@ -55,6 +55,7 @@ enum UserInterfaceInputField {
TextEditor,
ShowHiddenFiles,
GroupDirs,
FileFmt,
}
/// ### SetupTab

View File

@@ -35,7 +35,7 @@ use crate::host::Localhost;
use crossterm::event::DisableMouseCapture;
use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use std::io::{stdout, Stdout, Write};
use std::io::{stdout, Stdout};
use tui::backend::CrosstermBackend;
use tui::Terminal;

View File

@@ -27,6 +27,7 @@ extern crate chrono;
extern crate textwrap;
use chrono::prelude::*;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
/// ### fmt_pex
@@ -116,6 +117,39 @@ pub fn align_text_center(text: &str, width: u16) -> String {
.to_string()
}
/// ### elide_path
///
/// Elide a path if longer than width
/// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME}
pub fn fmt_path_elide(p: &Path, width: usize) -> String {
let fmt_path: String = format!("{}", p.display());
match fmt_path.len() > width as usize {
false => fmt_path,
true => {
// Elide
let ancestors_len: usize = p.ancestors().count();
let mut ancestors = p.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) = p.ancestors().nth(1) {
elided_path.push(parent.file_name().unwrap());
}
}
// Push file_name
if let Some(name) = p.file_name() {
elided_path.push(name);
}
format!("{}", elided_path.display())
}
}
}
#[cfg(test)]
mod tests {
@@ -169,4 +203,16 @@ mod tests {
String::from("18.192")
);
}
#[test]
#[cfg(any(target_os = "unix", target_os = "linux", target_os = "macos"))]
fn test_utils_fmt_path_elide() {
let p: &Path = &Path::new("/develop/pippo");
// Under max size
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/pippo"));
// Above max size, only one ancestor
assert_eq!(fmt_path_elide(p, 8), String::from("/develop/pippo"));
let p: &Path = &Path::new("/develop/pippo/foo/bar");
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/.../foo/bar"));
}
}