Merge pull request #288 from veeso/0.15

This commit is contained in:
Christian Visintin
2024-10-03 17:55:50 +02:00
committed by GitHub
70 changed files with 2676 additions and 2424 deletions

View File

@@ -22,21 +22,21 @@ jobs:
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
override: true
target: ${{ matrix.platform.target }}
targets: ${{ matrix.platform.target }}
- name: Build release
run: cargo build --release --target ${{ matrix.platform.target }}
- name: Prepare artifact files
run: |
mkdir -p .artifact
mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp
ls -l .artifact/
- name: "Upload artifact"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
retention-days: 1
name: ${{ matrix.platform.release_for }}
path: .artifact/*
path: .artifact/termscp

View File

@@ -1,45 +0,0 @@
name: Coverage
on:
pull_request:
paths-ignore:
- "*.md"
- "./site/**/*"
push:
paths-ignore:
- "*.md"
- "./site/**/*"
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev libsmbclient
- name: Setup nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Run tests (nightly)
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features github-actions --no-fail-fast
env:
CARGO_INCREMENTAL: "0"
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
- name: Coverage with grcov
id: coverage
uses: actions-rs/grcov@v0.1
- name: Coveralls
uses: coverallsapp/github-action@v1.1.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ${{ steps.coverage.outputs.report }}

View File

@@ -21,10 +21,9 @@ jobs:
- uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Run tests
uses: actions-rs/cargo@v1

View File

@@ -18,10 +18,9 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build
run: cargo build

View File

@@ -19,10 +19,9 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build
run: cargo build

View File

@@ -1,6 +1,7 @@
# Changelog
- [Changelog](#changelog)
- [0.15.0](#0150)
- [0.14.0](#0140)
- [0.13.0](#0130)
- [0.12.3](#0123)
@@ -36,6 +37,20 @@
---
## 0.15.0
Released on 03/10/2024
- [Issue 249](https://github.com/veeso/termscp/issues/249): The old *find* command has been replaced with a brand new explorer with support to 🪄 **Fuzzy search** 🪄. The command is still `<F>`.
- [Issue 283](https://github.com/veeso/termscp/issues/283): **Find command can now be cancelled** by pressing `<CTRL+C>`. While scanning the directory it will also display the current progress.
- [Issue 268](https://github.com/veeso/termscp/issues/268): 📦 **Pods and container explorer** 🐳 for Kube protocol.
- BREAKING ‼️ Kube address argument has changed to `namespace[@<cluster_url>][$<path>]`
- Pod and container argumets have been removed; from now on you will connect with the following syntax to the provided namespace: `/pod-name/container-name/path/to/file`
- [Issue 279](https://github.com/veeso/termscp/issues/279): do not clear screen
- [Issue 277](https://github.com/veeso/termscp/issues/277): Fix a bug in the configuration page, which caused being stuck if the added SSH key was empty
- [Issue 272](https://github.com/veeso/termscp/issues/272): `isolated-tests` feature to run tests for releasing on distributions which run in isolated environments
- [Issue 280](https://github.com/veeso/termscp/issues/280): Autocompletion when pressing `<TAB>` on the `Go to` popup.
## 0.14.0
Released on 17/07/2024

2544
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,18 +5,12 @@ description = "termscp is a feature rich terminal file transfer and explorer wit
edition = "2021"
homepage = "https://termscp.veeso.dev"
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
keywords = [
"scp-client",
"sftp-client",
"ftp-client",
"winscp",
"command-line-utility",
]
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
license = "MIT"
name = "termscp"
readme = "README.md"
repository = "https://github.com/veeso/termscp"
version = "0.14.0"
version = "0.15.0"
[package.metadata.rpm]
package = "termscp"
@@ -38,32 +32,37 @@ path = "src/main.rs"
[dependencies]
argh = "^0.1"
bitflags = "^2.1"
bytesize = "^1.1"
bitflags = "^2"
bytesize = "^1"
chrono = "^0.4"
content_inspector = "^0.2"
dirs = "^5.0"
edit = "^0.1"
filetime = "^0.2"
hostname = "^0.4"
keyring = { version = "^2.0", optional = true }
lazy-regex = "^3.1"
lazy_static = "^1.4"
keyring = { version = "^3", optional = true, features = [
"apple-native",
"windows-native",
"sync-secret-service",
] }
lazy-regex = "^3"
lazy_static = "^1"
log = "^0.4"
magic-crypt = "^3.1"
notify = "=4.0.17"
magic-crypt = "^3"
notify = "6"
notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
nucleo = "0.5"
open = "^5.0"
rand = "^0.8.5"
regex = "^1"
remotefs = "^0.2.0"
remotefs-aws-s3 = { version = "^0.2.4", default-features = false, features = [
remotefs = "^0.3"
remotefs-aws-s3 = { version = "^0.3", default-features = false, features = [
"find",
"rustls",
] }
remotefs-kube = "0.2"
remotefs-webdav = "^0.1.1"
rpassword = "^7.0"
remotefs-kube = "0.4"
remotefs-webdav = "^0.2"
rpassword = "^7"
self_update = { version = "^0.41", default-features = false, features = [
"rustls",
"archive-tar",
@@ -74,19 +73,19 @@ self_update = { version = "^0.41", default-features = false, features = [
serde = { version = "^1", features = ["derive"] }
simplelog = "^0.12"
ssh2-config = "^0.2"
tempfile = "^3.4"
tempfile = "^3"
thiserror = "^1"
tokio = { version = "=1.38.1", features = ["rt"] }
toml = "^0.8"
tui-realm-stdlib = "^1.3.1"
tuirealm = "^1.9.1"
unicode-width = "^0.1"
tui-realm-stdlib = "^1.3"
tuirealm = "^1.9"
unicode-width = "^0.2"
version-compare = "^0.2"
whoami = "^1.4"
wildmatch = "^2.1"
whoami = "^1.5"
wildmatch = "^2"
[dev-dependencies]
pretty_assertions = "^1.3"
pretty_assertions = "^1"
serial_test = "^3"
[build-dependencies]
@@ -95,21 +94,22 @@ cfg_aliases = "0.2"
[features]
default = ["smb", "with-keyring"]
github-actions = []
with-keyring = ["keyring"]
isolated-tests = []
smb = ["remotefs-smb"]
with-keyring = ["keyring"]
[target."cfg(not(target_os = \"macos\"))".dependencies]
remotefs-smb = { version = "^0.2", optional = true }
remotefs-smb = { version = "^0.3", optional = true }
[target."cfg(target_family = \"windows\")"]
[target."cfg(target_family = \"windows\")".dependencies]
remotefs-ftp = { version = "^0.1.2", features = ["native-tls"] }
remotefs-ssh = "^0.3.1"
remotefs-ftp = { version = "^0.2", features = ["native-tls"] }
remotefs-ssh = "^0.4"
[target."cfg(target_family = \"unix\")"]
[target."cfg(target_family = \"unix\")".dependencies]
remotefs-ftp = { version = "^0.1.2", features = ["vendored", "native-tls"] }
remotefs-ssh = { version = "^0.3.1", features = ["ssh2-vendored"] }
remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
remotefs-ssh = { version = "^0.4", features = ["ssh2-vendored"] }
users = "0.11.0"
[profile.dev]

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Developed by <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Current version: 0.14.0 (17/07/2024)</p>
<p align="center">Current version: 0.15.0 (03/10/2024)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
@@ -116,11 +116,6 @@
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
alt="Windows CI"
/></a>
<a href="https://coveralls.io/github/veeso/termscp"
><img
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
alt="Coveralls"
/></a>
</p>
---

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Entwickelt von <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Aktuelle Version: 0.14.0 (17/07/2024)</p>
<p align="center">Aktuelle Version: 0.15.0 (03/10/2024)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -133,7 +133,7 @@ s3://buckethead@eu-central-1:default:/assets
Falls Sie eine Verbindung zu Kube herstellen möchten, verwenden Sie die folgende Syntax
```txt
kube://<container>@<pod></path>
kube://[namespace][@<cluster_url>][$</path>]
```
#### SMB Adressargument

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Desarrollado por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Versión actual: 0.14.0 (17/07/2024)</p>
<p align="center">Versión actual: 0.15.0 (03/10/2024)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -110,7 +110,7 @@ s3://buckethead@eu-central-1:default:/assets
En caso de que quieras conectarte a Kube, utiliza la siguiente sintaxis
```txt
kube://<container>@<pod></path>
kube://[namespace][@<cluster_url>][$</path>]
```
#### Argumento de dirección de WebDAV

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Développé par <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Version actuelle: 0.14.0 (17/07/2024)</p>
<p align="center">Version actuelle: 0.15.0 (03/10/2024)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -108,7 +108,7 @@ s3://buckethead@eu-central-1:default:/assets
Si vous souhaitez vous connecter à Kube, utilisez la syntaxe suivante
```txt
kube://<container>@<pod></path>
kube://[namespace][@<cluster_url>][$</path>]
```
#### Argument d'adresse WebDAV

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Sviluppato da <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Versione corrente: 0.14.0 (17/07/2024)</p>
<p align="center">Versione corrente: 0.15.0 (03/10/2024)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -106,7 +106,7 @@ s3://buckethead@eu-central-1:default:/assets
Nel caso tu voglia connetterti a Kube usa la seguente sintassi
```txt
kube://<container>@<pod></path>
kube://[namespace][@<cluster_url>][$</path>]
```
#### Argomento indirizzo per WebDAV

View File

@@ -111,7 +111,7 @@ s3://buckethead@eu-central-1:default:/assets
In case you want to connect to Kube use the following syntax
```txt
kube://<container>@<pod></path>
kube://[namespace][@<cluster_url>][$</path>]
```
#### WebDAV address argument

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Desenvolvido por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Versão atual: 0.14.0 (17/07/2024)</p>
<p align="center">Versão atual: 0.15.0 (03/10/2024)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -111,7 +111,7 @@ s3://buckethead@eu-central-1:default:/assets
Caso queira se conectar ao Kube, use a seguinte sintaxe
```txt
kube://<container>@<pod></path>
kube://[namespace][@<cluster_url>][$</path>]
```
#### Argumento de Endereço do WebDAV

View File

@@ -71,7 +71,7 @@
</p>
<p align="center"><a href="https://veeso.dev/" target="_blank">@veeso</a> 开发</p>
<p align="center">当前版本: 0.14.0 (17/07/2024)</p>
<p align="center">当前版本: 0.15.0 (03/10/2024)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -108,7 +108,7 @@ s3://buckethead@eu-central-1:default:/assets
如果您想连接到 Kube请使用以下语法
```txt
kube://<container>@<pod></path>
kube://[namespace][@<cluster_url>][$</path>]
```
#### WebDAV 地址参数

View File

@@ -8,7 +8,7 @@
# -f, -y, --force, --yes
# Skip the confirmation prompt during installation
TERMSCP_VERSION="0.14.0"
TERMSCP_VERSION="0.15.0"
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"

View File

@@ -35,7 +35,7 @@
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
since last release, so if the latest version is not available yet,
you can install it downloading the ZIP file from</span>
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.14.0.nupkg"
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.15.0.nupkg"
target="_blank">Github</a>
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
</p>
@@ -74,7 +74,7 @@
On Debian based distros, you can install termscp using the Deb
package via:
</p>
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.14.0_amd64.deb</span>
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.15.0_amd64.deb</span>
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
</div>
<h3>

View File

@@ -12,7 +12,7 @@
</button>
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
<p class="text-lg">
<span translate="intro.versionAlert">termscp 0.14.0 is NOW out! Download it from</span>&nbsp;
<span translate="intro.versionAlert">termscp 0.15.0 is NOW out! Download it from</span>&nbsp;
<a href="/get-started.html" translate="intro.here">here!</a>
</p>
</div>

View File

@@ -12,7 +12,7 @@
"intro": {
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "Get started →",
"versionAlert": "termscp 0.14.0 is NOW out! Download it from",
"versionAlert": "termscp 0.15.0 is NOW out! Download it from",
"here": "here",
"features": {
"handy": {

View File

@@ -12,7 +12,7 @@
"intro": {
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "Para iniciar →",
"versionAlert": "termscp 0.14.0 ya está disponible! Descárgalo desde",
"versionAlert": "termscp 0.15.0 ya está disponible! Descárgalo desde",
"here": "aquì",
"features": {
"handy": {

View File

@@ -12,7 +12,7 @@
"intro": {
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "Pour commencer →",
"versionAlert": "termscp 0.14.0 est maintenant sorti! Télécharge-le depuis",
"versionAlert": "termscp 0.15.0 est maintenant sorti! Télécharge-le depuis",
"here": "ici",
"features": {
"handy": {

View File

@@ -12,7 +12,7 @@
"intro": {
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
"getStarted": "Installa termscp →",
"versionAlert": "termscp 0.14.0 è ORA disponbile! Scaricalo da",
"versionAlert": "termscp 0.15.0 è ORA disponbile! Scaricalo da",
"here": "qui",
"features": {
"handy": {

View File

@@ -12,7 +12,7 @@
"intro": {
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "开始 →",
"versionAlert": "termscp 0.14.0 现已发布! 从下载",
"versionAlert": "termscp 0.15.0 现已发布! 从下载",
"here": "这里",
"features": {
"handy": {

View File

@@ -351,8 +351,6 @@ mod tests {
#[test]
fn bookmark_from_kube_ftparams() {
let params = ProtocolParams::Kube(KubeProtocolParams {
pod: "pod".to_string(),
container: "container".to_string(),
namespace: Some("default".to_string()),
username: Some("root".to_string()),
cluster_url: Some("https://localhost:6443".to_string()),
@@ -368,8 +366,6 @@ mod tests {
assert!(bookmark.username.is_none());
assert!(bookmark.password.is_none());
let kube: &KubeParams = bookmark.kube.as_ref().unwrap();
assert_eq!(kube.pod_name.as_str(), "pod");
assert_eq!(kube.container.as_str(), "container");
assert_eq!(kube.namespace.as_deref().unwrap(), "default");
assert_eq!(
kube.cluster_url.as_deref().unwrap(),
@@ -494,8 +490,6 @@ mod tests {
remote_path: Some(PathBuf::from("/tmp")),
local_path: Some(PathBuf::from("/usr")),
kube: Some(KubeParams {
pod_name: String::from("pod"),
container: String::from("container"),
namespace: Some(String::from("default")),
cluster_url: Some(String::from("https://localhost:6443")),
username: Some(String::from("root")),
@@ -516,7 +510,6 @@ mod tests {
std::path::Path::new("/usr")
);
let gparams = params.params.kube_params().unwrap();
assert_eq!(gparams.pod.as_str(), "pod");
assert_eq!(gparams.namespace.as_deref().unwrap(), "default");
assert_eq!(
gparams.cluster_url.as_deref().unwrap(),

View File

@@ -5,8 +5,6 @@ use crate::filetransfer::params::KubeProtocolParams;
/// Extra Connection parameters for Kube protocol
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
pub struct KubeParams {
pub pod_name: String,
pub container: String,
pub namespace: Option<String>,
pub cluster_url: Option<String>,
pub username: Option<String>,
@@ -17,8 +15,6 @@ pub struct KubeParams {
impl From<KubeParams> for KubeProtocolParams {
fn from(value: KubeParams) -> Self {
Self {
pod: value.pod_name,
container: value.container,
namespace: value.namespace,
cluster_url: value.cluster_url,
username: value.username,
@@ -31,8 +27,6 @@ impl From<KubeParams> for KubeProtocolParams {
impl From<KubeProtocolParams> for KubeParams {
fn from(value: KubeProtocolParams) -> Self {
Self {
pod_name: value.pod,
container: value.container,
namespace: value.namespace,
cluster_url: value.cluster_url,
username: value.username,

View File

@@ -412,8 +412,6 @@ mod tests {
assert_eq!(host.password, None);
assert_eq!(host.protocol, FileTransferProtocol::Kube);
let kube = host.kube.as_ref().unwrap();
assert_eq!(kube.pod_name.as_str(), "my-pod");
assert_eq!(kube.container.as_str(), "my-container");
assert_eq!(kube.namespace.as_deref().unwrap(), "my-namespace");
assert_eq!(kube.cluster_url.as_deref().unwrap(), "https://my-cluster");
assert_eq!(kube.username.as_deref().unwrap(), "my-username");
@@ -515,8 +513,6 @@ mod tests {
s3: None,
smb: None,
kube: Some(KubeParams {
pod_name: "my-pod".to_string(),
container: "my-container".to_string(),
namespace: Some("my-namespace".to_string()),
cluster_url: Some("https://my-cluster".to_string()),
username: Some("my-username".to_string()),
@@ -593,6 +589,22 @@ mod tests {
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err());
}
#[test]
fn test_should_deserialize_v14_pod_bookmark() {
let toml = create_v14_pod_bookmark();
toml.as_file().sync_all().unwrap();
toml.as_file().rewind().unwrap();
let deserialized: UserHosts = deserialize(Box::new(toml)).unwrap();
let kube = deserialized.bookmarks.get("pod").unwrap();
assert_eq!(kube.protocol, FileTransferProtocol::Kube);
let kube = kube.kube.as_ref().unwrap();
assert_eq!(kube.namespace.as_deref().unwrap(), "my-namespace");
assert_eq!(kube.cluster_url.as_deref().unwrap(), "https://my-cluster");
assert_eq!(kube.username.as_deref().unwrap(), "my-username");
assert_eq!(kube.client_cert.as_deref().unwrap(), "my-cert");
assert_eq!(kube.client_key.as_deref().unwrap(), "my-key");
}
fn create_good_toml_bookmarks() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
@@ -617,8 +629,6 @@ mod tests {
[bookmarks.pod]
protocol = "KUBE"
[bookmarks.pod.kube]
pod_name = "my-pod"
container = "my-container"
namespace = "my-namespace"
cluster_url = "https://my-cluster"
username = "my-username"
@@ -644,6 +654,29 @@ mod tests {
tmpfile
}
fn create_v14_pod_bookmark() -> tempfile::NamedTempFile {
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[bookmarks]
[bookmarks.pod]
protocol = "KUBE"
[bookmarks.pod.kube]
pod_name = "my-pod"
container = "my-container"
namespace = "my-namespace"
cluster_url = "https://my-cluster"
username = "my-username"
client_cert = "my-cert"
client_key = "my-key"
[recents]
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
//write!(tmpfile, "[bookmarks]\nraspberrypi2 = {{ address = \"192.168.1.31\", port = 22, protocol = \"SFTP\", username = \"root\" }}\nmsi-estrem = {{ address = \"192.168.1.30\", port = 22, protocol = \"SFTP\", username = \"cvisintin\" }}\naws-server-prod1 = {{ address = \"51.23.67.12\", port = 21, protocol = \"FTPS\", username = \"aws001\" }}\n\n[recents]\nISO20201215T094000Z = {{ address = \"172.16.104.10\", port = 22, protocol = \"SCP\", username = \"root\" }}\n");
tmpfile
}
fn create_bad_toml_bookmarks() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();

View File

@@ -30,6 +30,7 @@ pub enum FileSorting {
ModifyTime,
CreationTime,
Size,
None,
}
/// GroupDirs defines how directories should be grouped in sorting files
@@ -178,6 +179,7 @@ impl FileExplorer {
FileSorting::CreationTime => self.sort_files_by_creation_time(),
FileSorting::ModifyTime => self.sort_files_by_mtime(),
FileSorting::Size => self.sort_files_by_size(),
FileSorting::None => {}
}
// Directories first (NOTE: MUST COME AFTER OTHER SORTING)
// Group directories if necessary
@@ -245,6 +247,7 @@ impl std::fmt::Display for FileSorting {
FileSorting::ModifyTime => "by_mtime",
FileSorting::Name => "by_name",
FileSorting::Size => "by_size",
FileSorting::None => "none",
}
)
}

View File

@@ -8,7 +8,7 @@ use std::sync::Arc;
use remotefs::RemoteFs;
use remotefs_aws_s3::AwsS3Fs;
use remotefs_ftp::FtpFs;
use remotefs_kube::KubeFs;
use remotefs_kube::KubeMultiPodFs as KubeFs;
#[cfg(smb_unix)]
use remotefs_smb::SmbOptions;
#[cfg(smb)]
@@ -119,7 +119,7 @@ impl Builder {
.build()
.expect("Unable to create tokio runtime"),
);
let kube_fs = KubeFs::new(&params.pod, &params.container, &rt);
let kube_fs = KubeFs::new(&rt);
if let Some(config) = params.config() {
kube_fs.config(config)
} else {
@@ -281,8 +281,6 @@ mod test {
#[test]
fn test_should_build_kube_fs() {
let params = ProtocolParams::Kube(KubeProtocolParams {
pod: "pod".to_string(),
container: "container".to_string(),
namespace: Some("namespace".to_string()),
cluster_url: Some("cluster_url".to_string()),
username: Some("username".to_string()),

View File

@@ -3,8 +3,6 @@ use remotefs_kube::Config;
/// Protocol params used by WebDAV
#[derive(Debug, Clone)]
pub struct KubeProtocolParams {
pub pod: String,
pub container: String,
pub namespace: Option<String>,
pub cluster_url: Option<String>,
pub username: Option<String>,

View File

@@ -16,7 +16,6 @@ use filetime::{self, FileTime};
use remotefs::fs::UnixPex;
use remotefs::fs::{File, FileType, Metadata};
use thiserror::Error;
use wildmatch::WildMatch;
// Locals
use crate::utils::path;
@@ -112,7 +111,7 @@ impl Localhost {
));
}
// Retrieve files for provided path
host.files = match host.scan_dir(host.wrkdir.as_path()) {
host.files = match host.list_dir(host.wrkdir.as_path()) {
Ok(files) => files,
Err(err) => {
error!(
@@ -131,12 +130,6 @@ impl Localhost {
self.wrkdir.clone()
}
/// List files in current directory
#[allow(dead_code)]
pub fn list_dir(&self) -> Vec<File> {
self.files.clone()
}
/// Change working directory with the new provided directory
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
let new_dir: PathBuf = self.to_path(new_dir);
@@ -164,7 +157,7 @@ impl Localhost {
// Change dir
self.wrkdir = new_dir;
// Scan new directory
self.files = match self.scan_dir(self.wrkdir.as_path()) {
self.files = match self.list_dir(self.wrkdir.as_path()) {
Ok(files) => files,
Err(err) => {
error!("Could not scan new directory: {}", err);
@@ -204,7 +197,7 @@ impl Localhost {
Ok(_) => {
// Update dir
if dir_name.is_relative() {
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
}
info!("Created directory {}", dir_path.display());
Ok(())
@@ -237,7 +230,7 @@ impl Localhost {
match std::fs::remove_dir_all(entry.path()) {
Ok(_) => {
// Update dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
info!("Removed directory {}", entry.path().display());
Ok(())
}
@@ -265,7 +258,7 @@ impl Localhost {
match std::fs::remove_file(entry.path()) {
Ok(_) => {
// Update dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
info!("Removed file {}", entry.path().display());
Ok(())
}
@@ -286,7 +279,7 @@ impl Localhost {
match std::fs::rename(entry.path(), dst_path) {
Ok(_) => {
// Scan dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
debug!(
"Moved file {} to {}",
entry.path().display(),
@@ -327,7 +320,7 @@ impl Localhost {
self.mkdir(dst.as_path())?;
}
// Scan dir
let dir_files: Vec<File> = self.scan_dir(entry.path())?;
let dir_files: Vec<File> = self.list_dir(entry.path())?;
// Iterate files
for dir_entry in dir_files.iter() {
// Calculate dst
@@ -362,11 +355,11 @@ impl Localhost {
match dst.is_dir() {
true => {
if dst == self.pwd().as_path() {
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
} else if let Some(parent) = dst.parent() {
// If parent is pwd, scan directory
if parent == self.pwd().as_path() {
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
}
}
}
@@ -374,7 +367,7 @@ impl Localhost {
if let Some(parent) = dst.parent() {
// If parent is pwd, scan directory
if parent == self.pwd().as_path() {
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
}
}
}
@@ -557,7 +550,7 @@ impl Localhost {
}
/// Get content of the current directory as a list of fs entry
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<File>, HostError> {
pub fn list_dir(&self, dir: &Path) -> Result<Vec<File>, HostError> {
info!("Reading directory {}", dir.display());
match std::fs::read_dir(dir) {
Ok(e) => {
@@ -579,12 +572,6 @@ impl Localhost {
}
}
/// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course.
/// The `search` argument supports wilcards ('*', '?')
pub fn find(&self, search: &str) -> Result<Vec<File>, HostError> {
self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search))
}
/// Create a symlink at path pointing at target
#[cfg(unix)]
pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> {
@@ -600,41 +587,6 @@ impl Localhost {
})
}
// -- privates
/// Recursive call for `find` method.
/// Search in current directory for files which match `filter`.
/// If a directory is found in current directory, `iter_search` will be called using that dir as argument.
fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result<Vec<File>, HostError> {
// Scan directory
let mut drained: Vec<File> = Vec::new();
match self.scan_dir(dir) {
Err(err) => Err(err),
Ok(entries) => {
// Iter entries
/* For each entry:
- if is dir: call iter_search with `dir`
- push `iter_search` result to `drained`
- if is file: check if it matches `filter`
- if it matches `filter`: push to to filter
*/
for entry in entries.into_iter() {
if entry.is_dir() {
// If directory matches; push directory to drained
let next_path = entry.path().to_path_buf();
if filter.matches(entry.name().as_str()) {
drained.push(entry);
}
drained.append(&mut self.iter_search(next_path.as_path(), filter)?);
} else if filter.matches(entry.name().as_str()) {
drained.push(entry);
}
}
Ok(drained)
}
}
}
/// Convert path to absolute path
fn to_path(&self, p: &Path) -> PathBuf {
path::absolutize(self.wrkdir.as_path(), p)
@@ -658,7 +610,7 @@ mod tests {
use super::*;
#[cfg(unix)]
use crate::utils::test_helpers::make_fsentry;
use crate::utils::test_helpers::{create_sample_file, make_dir_at, make_file_at};
use crate::utils::test_helpers::{create_sample_file, make_file_at};
#[test]
fn test_host_error_new() {
@@ -711,19 +663,6 @@ mod tests {
assert_eq!(host.pwd(), PathBuf::from("/dev"));
}
#[test]
#[cfg(unix)]
fn test_host_localhost_list_files() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
// Scan dir
let entries = std::fs::read_dir(PathBuf::from("/dev").as_path()).unwrap();
let mut counter: usize = 0;
for _ in entries {
counter += 1;
}
assert_eq!(host.list_dir().len(), counter);
}
#[test]
#[cfg(unix)]
fn test_host_localhost_change_dir() {
@@ -813,7 +752,7 @@ mod tests {
.is_ok());
// Get dir
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
// Verify files
let file_0: &File = files.get(0).unwrap();
if file_0.name() == *"foo.txt" {
@@ -841,10 +780,10 @@ mod tests {
fn test_host_localhost_mkdir() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 0); // There should be 0 files now
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
// Try to re-create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
@@ -868,17 +807,17 @@ mod tests {
// Create sample file
assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
// Remove file
assert!(host.remove(files.get(0).unwrap()).is_ok());
// There should be 0 files now
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 0); // There should be 0 files now
// Create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
// Delete directory
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
assert!(host.remove(files.get(0).unwrap()).is_ok());
// Remove unexisting directory
@@ -899,7 +838,7 @@ mod tests {
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str());
assert!(StdFile::create(src_path.as_path()).is_ok());
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
assert_eq!(files.get(0).unwrap().name(), "foo.txt");
// Rename file
@@ -909,7 +848,7 @@ mod tests {
.rename(files.get(0).unwrap(), dst_path.as_path())
.is_ok());
// There should be still 1 file now, but named bar.txt
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 0 files now
assert_eq!(files.get(0).unwrap().name(), "bar.txt");
// Fail
@@ -1084,39 +1023,6 @@ mod tests {
assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5"));
}
#[test]
fn test_host_find() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let dir_path: &Path = tmpdir.path();
// Make files
assert!(make_file_at(dir_path, "pippo.txt").is_ok());
assert!(make_file_at(dir_path, "foo.jpg").is_ok());
// Make nested struct
assert!(make_dir_at(dir_path, "examples").is_ok());
let mut subdir: PathBuf = PathBuf::from(dir_path);
subdir.push("examples/");
assert!(make_file_at(subdir.as_path(), "omar.txt").is_ok());
assert!(make_file_at(subdir.as_path(), "errors.txt").is_ok());
assert!(make_file_at(subdir.as_path(), "screenshot.png").is_ok());
assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok());
let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
// Find txt files
let mut result: Vec<File> = host.find("*.txt").ok().unwrap();
result.sort_by_key(|x: &File| x.name().to_lowercase());
// There should be 3 entries
assert_eq!(result.len(), 3);
// Check names (they should be sorted alphabetically already; NOTE: examples/ comes before pippo.txt)
assert_eq!(result[0].name(), "errors.txt");
assert_eq!(result[1].name(), "omar.txt");
assert_eq!(result[2].name(), "pippo.txt");
// Search for directory
let mut result: Vec<File> = host.find("examples*").ok().unwrap();
result.sort_by_key(|x: &File| x.name().to_lowercase());
assert_eq!(result.len(), 2);
assert_eq!(result[0].name(), "examples");
assert_eq!(result[1].name(), "examples.csv");
}
#[cfg(unix)]
#[test]
fn should_create_symlink() {

View File

@@ -145,10 +145,13 @@ mod test {
}
#[test]
#[cfg(not(all(
any(target_os = "macos", target_os = "freebsd"),
feature = "github-actions"
)))]
#[cfg(all(
not(all(
any(target_os = "macos", target_os = "freebsd"),
feature = "github-actions"
)),
not(feature = "isolated-tests")
))]
fn auto_update() {
// Wno version
assert_eq!(
@@ -162,10 +165,13 @@ mod test {
}
#[test]
#[cfg(not(all(
any(target_os = "macos", target_os = "freebsd"),
feature = "github-actions"
)))]
#[cfg(all(
not(all(
any(target_os = "macos", target_os = "freebsd"),
feature = "github-actions"
)),
not(feature = "isolated-tests")
))]
fn check_for_updates() {
println!("{:?}", Update::is_new_version_available());
assert!(Update::is_new_version_available().is_ok());

View File

@@ -82,6 +82,7 @@ mod tests {
use super::*;
#[test]
#[cfg(not(feature = "isolated-tests"))]
fn test_system_keys_keyringstorage() {
let username: String = username();
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
@@ -89,17 +90,17 @@ mod tests {
let app_name: &str = "termscp-test2";
let secret: &str = "Th15-15/My-Супер-Секрет";
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
let _ = kring.delete_password();
let _ = kring.delete_credential();
drop(kring);
// Secret should not exist
assert!(storage.get_key(app_name).is_err());
// Write secret
assert!(storage.set_key(app_name, secret).is_ok());
// Get secret
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
assert_eq!(storage.get_key(app_name).unwrap().as_str(), secret);
// Delete the key manually...
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
assert!(kring.delete_password().is_ok());
assert!(kring.delete_credential().is_ok());
}
}

View File

@@ -12,7 +12,7 @@ use std::time::Duration;
pub use change::FsChange;
use notify::{
watcher, DebouncedEvent, Error as WatcherError, RecommendedWatcher, RecursiveMode, Watcher,
Config, Error as WatcherError, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
};
use thiserror::Error;
@@ -27,6 +27,8 @@ pub enum FsWatcherError {
PathNotWatched,
#[error("unable to watch path, since it's already watched")]
PathAlreadyWatched,
#[error("unknown event: {0}")]
UnknownEvent(&'static str),
#[error("worker error: {0}")]
WorkerError(WatcherError),
}
@@ -37,10 +39,61 @@ impl From<WatcherError> for FsWatcherError {
}
}
/// Describes an event that can be received from the `FsWatcher`
#[derive(Debug, Clone, PartialEq, Eq)]
enum FsWatcherEvent {
Rename { source: PathBuf, dest: PathBuf },
Remove(PathBuf),
Create(PathBuf),
Modify(PathBuf),
Other,
}
impl TryFrom<Event> for FsWatcherEvent {
type Error = &'static str;
fn try_from(ev: Event) -> Result<Self, Self::Error> {
match ev.kind {
EventKind::Any | EventKind::Access(_) | EventKind::Other => Ok(Self::Other),
EventKind::Create(_) => {
if ev.paths.len() == 2 {
Ok(Self::Rename {
source: ev.paths[0].clone(),
dest: ev.paths[1].clone(),
})
} else if let Some(p) = ev.paths.first() {
Ok(Self::Create(p.clone()))
} else {
Err("No path found")
}
}
EventKind::Modify(_) => {
if ev.paths.len() == 2 {
Ok(Self::Rename {
source: ev.paths[0].clone(),
dest: ev.paths[1].clone(),
})
} else if let Some(p) = ev.paths.first() {
Ok(Self::Modify(p.clone()))
} else {
Err("No path found")
}
}
EventKind::Remove(_) => {
if let Some(p) = ev.paths.first() {
Ok(Self::Remove(p.clone()))
} else {
Err("No path found")
}
}
}
}
}
/// File system watcher
pub struct FsWatcher {
paths: HashMap<PathBuf, PathBuf>,
receiver: Receiver<DebouncedEvent>,
receiver: Receiver<notify::Result<Event>>,
watcher: RecommendedWatcher,
}
@@ -52,29 +105,32 @@ impl FsWatcher {
Ok(Self {
paths: HashMap::default(),
receiver,
watcher: watcher(tx, delay)?,
watcher: RecommendedWatcher::new(tx, Config::default().with_poll_interval(delay))?,
})
}
/// Poll searching for the first available disk change
pub fn poll(&self) -> FsWatcherResult<Option<FsChange>> {
match self.receiver.recv_timeout(Duration::from_millis(1)) {
Ok(DebouncedEvent::Rename(source, dest)) => Ok(self.build_fs_move(source, dest)),
Ok(DebouncedEvent::Remove(p)) => Ok(self.build_fs_remove(p)),
Ok(DebouncedEvent::Chmod(p) | DebouncedEvent::Create(p) | DebouncedEvent::Write(p)) => {
Ok(self.build_fs_update(p))
}
Ok(
DebouncedEvent::Rescan
| DebouncedEvent::NoticeRemove(_)
| DebouncedEvent::NoticeWrite(_),
) => Ok(None),
Ok(DebouncedEvent::Error(e, _)) => {
error!("FsWatcher reported error: {}", e);
Err(e.into())
}
Err(RecvTimeoutError::Timeout) => Ok(None),
let res = match self.receiver.recv_timeout(Duration::from_millis(1)) {
Ok(res) => res,
Err(RecvTimeoutError::Timeout) => return Ok(None),
Err(RecvTimeoutError::Disconnected) => panic!("File watcher died"),
};
// convert event to FsChange
let event = res
.map(FsWatcherEvent::try_from)
.map_err(FsWatcherError::from)?
.map_err(FsWatcherError::UnknownEvent)?;
match event {
FsWatcherEvent::Rename { source, dest } => Ok(self.build_fs_move(source, dest)),
FsWatcherEvent::Remove(p) => Ok(self.build_fs_remove(p)),
FsWatcherEvent::Modify(p) | FsWatcherEvent::Create(p) => Ok(self.build_fs_update(p)),
FsWatcherEvent::Other => {
debug!("unknown event");
Ok(None)
}
}
}

View File

@@ -193,8 +193,6 @@ impl AuthActivity {
}
fn load_bookmark_kube_into_gui(&mut self, params: KubeProtocolParams) {
self.mount_kube_pod_name(params.pod.as_str());
self.mount_kube_container(&params.container);
self.mount_kube_cluster_url(params.cluster_url.as_deref().unwrap_or(""));
self.mount_kube_namespace(params.namespace.as_deref().unwrap_or(""));
self.mount_kube_client_cert(params.client_cert.as_deref().unwrap_or(""));

View File

@@ -832,40 +832,6 @@ impl Component<Msg, NoUserEvent> for InputWebDAVUri {
// kube
#[derive(MockComponent)]
pub struct InputKubePodName {
component: Input,
}
impl InputKubePodName {
pub fn new(bucket: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("pod-name", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Pod name", Alignment::Left)
.input_type(InputType::Text)
.value(bucket),
}
}
}
impl Component<Msg, NoUserEvent> for InputKubePodName {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Ui(UiMsg::KubePodNameBlurDown),
Msg::Ui(UiMsg::KubePodNameBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct InputKubeNamespace {
component: Input,
@@ -937,40 +903,6 @@ impl Component<Msg, NoUserEvent> for InputKubeClusterUrl {
}
}
#[derive(MockComponent)]
pub struct InputKubeContainer {
component: Input,
}
impl InputKubeContainer {
pub fn new(bucket: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder("container", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Kube container", Alignment::Left)
.input_type(InputType::Text)
.value(bucket),
}
}
}
impl Component<Msg, NoUserEvent> for InputKubeContainer {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Ui(UiMsg::KubeContainerBlurDown),
Msg::Ui(UiMsg::KubeContainerBlurUp),
)
}
}
#[derive(MockComponent)]
pub struct InputKubeUsername {
component: Input,

View File

@@ -16,12 +16,11 @@ pub use bookmarks::{
#[cfg(unix)]
pub use form::InputSmbWorkgroup;
pub use form::{
InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeContainer,
InputKubeNamespace, InputKubePodName, InputKubeUsername, InputLocalDirectory, InputPassword,
InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint,
InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, ProtocolRadio,
RadioS3NewPathStyle,
InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeNamespace,
InputKubeUsername, InputLocalDirectory, InputPassword, InputPort, InputRemoteDirectory,
InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile, InputS3Region,
InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputSmbShare,
InputUsername, InputWebDAVUri, ProtocolRadio, RadioS3NewPathStyle,
};
pub use popup::{
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,

View File

@@ -85,9 +85,7 @@ impl AuthActivity {
/// Get input values from fields or return an error if fields are invalid to work as aws s3
pub(super) fn collect_kube_host_params(&self) -> Result<FileTransferParams, &'static str> {
let params = self.get_kube_params_input();
if params.pod.is_empty() {
return Err("Invalid pod name");
}
Ok(FileTransferParams {
protocol: FileTransferProtocol::Kube,
params: ProtocolParams::Kube(params),

View File

@@ -48,8 +48,6 @@ pub enum Id {
InfoPopup,
InstallUpdatePopup,
Keybindings,
KubePodName,
KubeContainer,
KubeNamespace,
KubeClusterUrl,
KubeUsername,
@@ -119,10 +117,6 @@ pub enum UiMsg {
CloseKeybindingsPopup,
CloseQuitPopup,
CloseSaveBookmark,
KubePodNameBlurDown,
KubePodNameBlurUp,
KubeContainerBlurDown,
KubeContainerBlurUp,
KubeNamespaceBlurDown,
KubeNamespaceBlurUp,
KubeClusterUrlBlurDown,

View File

@@ -70,7 +70,7 @@ impl AuthActivity {
InputMask::Generic => &Id::Password,
InputMask::Smb => &Id::Password,
InputMask::AwsS3 => &Id::S3Bucket,
InputMask::Kube => &Id::KubePodName,
InputMask::Kube => &Id::KubeNamespace,
InputMask::WebDAV => &Id::Password,
})
.is_ok());
@@ -84,7 +84,7 @@ impl AuthActivity {
InputMask::Generic => &Id::Password,
InputMask::Smb => &Id::Password,
InputMask::AwsS3 => &Id::S3Bucket,
InputMask::Kube => &Id::KubePodName,
InputMask::Kube => &Id::KubeNamespace,
InputMask::WebDAV => &Id::Password,
})
.is_ok());
@@ -210,7 +210,7 @@ impl AuthActivity {
InputMask::Generic => &Id::Address,
InputMask::Smb => &Id::Address,
InputMask::AwsS3 => &Id::S3Bucket,
InputMask::Kube => &Id::KubePodName,
InputMask::Kube => &Id::KubeNamespace,
InputMask::WebDAV => &Id::WebDAVUri,
})
.is_ok());
@@ -305,23 +305,11 @@ impl AuthActivity {
UiMsg::KubeClientKeyBlurUp => {
assert!(self.app.active(&Id::KubeClientCert).is_ok());
}
UiMsg::KubeContainerBlurDown => {
assert!(self.app.active(&Id::KubeNamespace).is_ok());
}
UiMsg::KubeContainerBlurUp => {
assert!(self.app.active(&Id::KubePodName).is_ok());
}
UiMsg::KubePodNameBlurDown => {
assert!(self.app.active(&Id::KubeContainer).is_ok());
}
UiMsg::KubePodNameBlurUp => {
assert!(self.app.active(&Id::Protocol).is_ok());
}
UiMsg::KubeNamespaceBlurDown => {
assert!(self.app.active(&Id::KubeClusterUrl).is_ok());
}
UiMsg::KubeNamespaceBlurUp => {
assert!(self.app.active(&Id::KubeContainer).is_ok());
assert!(self.app.active(&Id::Protocol).is_ok());
}
UiMsg::KubeClusterUrlBlurDown => {
assert!(self.app.active(&Id::KubeUsername).is_ok());

View File

@@ -64,9 +64,7 @@ impl AuthActivity {
self.mount_kube_client_cert("");
self.mount_kube_client_key("");
self.mount_kube_cluster_url("");
self.mount_kube_container("");
self.mount_kube_namespace("");
self.mount_kube_pod_name("");
self.mount_kube_username("");
self.mount_smb_share("");
#[cfg(unix)]
@@ -149,86 +147,18 @@ impl AuthActivity {
.direction(Direction::Vertical)
.split(main_chunks[0]);
// Input mask chunks
let input_mask = match self.input_mask() {
InputMask::AwsS3 => Layout::default()
.constraints(
[
Constraint::Length(3), // bucket
Constraint::Length(3), // region
Constraint::Length(3), // profile
Constraint::Length(3), // access_key
Constraint::Length(3), // remote directory
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
InputMask::Kube => Layout::default()
.constraints([
Constraint::Length(3), // ...
Constraint::Length(3), // ...
Constraint::Length(3), // ...
Constraint::Length(3), // ...
Constraint::Length(3), // remote directory
])
.direction(Direction::Vertical)
.split(auth_chunks[4]),
InputMask::Generic => Layout::default()
.constraints(
[
Constraint::Length(3), // address
Constraint::Length(3), // port
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // remote directory
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
#[cfg(unix)]
InputMask::Smb => Layout::default()
.constraints(
[
Constraint::Length(3), // address
Constraint::Length(3), // port
Constraint::Length(3), // share
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // workgroup
Constraint::Length(3), // remote directory
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
#[cfg(windows)]
InputMask::Smb => Layout::default()
.constraints(
[
Constraint::Length(3), // address
Constraint::Length(3), // share
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // remote directory
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
InputMask::WebDAV => Layout::default()
.constraints(
[
Constraint::Length(3), // uri
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // dir
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
};
let input_mask = Layout::default()
.constraints(
[
Constraint::Length(3), // uri
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // dir
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]);
// Create bookmark chunks
let bookmark_chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
@@ -347,7 +277,7 @@ impl AuthActivity {
.constraints(
[
Constraint::Length(3), // Input form
Constraint::Length(2), // Yes/No
Constraint::Length(4), // Yes/No
]
.as_ref(),
)
@@ -816,30 +746,6 @@ impl AuthActivity {
.is_ok());
}
pub(super) fn mount_kube_pod_name(&mut self, value: &str) {
let color = self.theme().auth_address;
assert!(self
.app
.remount(
Id::KubePodName,
Box::new(components::InputKubePodName::new(value, color)),
vec![]
)
.is_ok());
}
pub(super) fn mount_kube_container(&mut self, value: &str) {
let color = self.theme().auth_password;
assert!(self
.app
.remount(
Id::KubeContainer,
Box::new(components::InputKubeContainer::new(value, color)),
vec![]
)
.is_ok());
}
pub(super) fn mount_kube_namespace(&mut self, value: &str) {
let color = self.theme().auth_port;
assert!(self
@@ -974,16 +880,12 @@ impl AuthActivity {
/// Collect s3 input values from view
pub(super) fn get_kube_params_input(&self) -> KubeProtocolParams {
let pod = self.get_input_kube_pod_name();
let container = self.get_input_kube_container();
let namespace = self.get_input_kube_namespace();
let cluster_url = self.get_input_kube_cluster_url();
let username = self.get_input_kube_username();
let client_cert = self.get_input_kube_client_cert();
let client_key = self.get_input_kube_client_key();
KubeProtocolParams {
pod,
container,
namespace,
cluster_url,
username,
@@ -1069,10 +971,7 @@ impl AuthActivity {
pub(super) fn get_input_port(&self) -> u16 {
match self.app.state(&Id::Port) {
Ok(State::One(StateValue::String(x))) => match u16::from_str(x.as_str()) {
Ok(v) => v,
_ => 0,
},
Ok(State::One(StateValue::String(x))) => u16::from_str(x.as_str()).unwrap_or_default(),
_ => 0,
}
}
@@ -1154,20 +1053,6 @@ impl AuthActivity {
)
}
pub(super) fn get_input_kube_pod_name(&self) -> String {
match self.app.state(&Id::KubePodName) {
Ok(State::One(StateValue::String(x))) => x,
_ => String::new(),
}
}
pub(super) fn get_input_kube_container(&self) -> String {
match self.app.state(&Id::KubeContainer) {
Ok(State::One(StateValue::String(x))) => x,
_ => String::new(),
}
}
pub(super) fn get_input_kube_namespace(&self) -> Option<String> {
match self.app.state(&Id::KubeNamespace) {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x),
@@ -1285,15 +1170,13 @@ impl AuthActivity {
}
ProtocolParams::Kube(params) => {
format!(
"{}://{}@{}{}{}",
"{}://{}{}",
protocol,
params.container,
params.pod,
params
.namespace
.as_deref()
.map(|x| format!("/{x}"))
.unwrap_or_default(),
.unwrap_or_else(|| String::from("default")),
params
.cluster_url
.as_deref()
@@ -1389,18 +1272,6 @@ impl AuthActivity {
/// Get the visible element in the kube form, based on current focus
fn get_kube_view(&self) -> [Id; 4] {
match self.app.focus() {
Some(&Id::KubePodName) => [
Id::KubePodName,
Id::KubeContainer,
Id::KubeNamespace,
Id::KubeClusterUrl,
],
Some(&Id::KubeUsername) => [
Id::KubeContainer,
Id::KubeNamespace,
Id::KubeClusterUrl,
Id::KubeUsername,
],
Some(&Id::KubeClientCert) => [
Id::KubeNamespace,
Id::KubeClusterUrl,
@@ -1426,10 +1297,10 @@ impl AuthActivity {
Id::LocalDirectory,
],
_ => [
Id::KubePodName,
Id::KubeContainer,
Id::KubeNamespace,
Id::KubeClusterUrl,
Id::KubeUsername,
Id::KubeClientCert,
],
}
}

View File

@@ -9,23 +9,10 @@ use super::super::browser::FileExplorerTab;
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
impl FileTransferActivity {
pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<File>, String> {
match self.host.find(input.as_str()) {
Ok(entries) => Ok(entries),
Err(err) => Err(format!("Could not search for files: {err}")),
}
}
pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<File>, String> {
match self.client.as_mut().find(input.as_str()) {
Ok(entries) => Ok(entries),
Err(err) => Err(format!("Could not search for files: {err}")),
}
}
pub(crate) fn action_find_changedir(&mut self) {
// Match entry
if let SelectedFile::One(entry) = self.get_found_selected_entries() {
debug!("Changedir to: {}", entry.name());
// Get path: if a directory, use directory path; if it is a File, get parent path
let path = if entry.is_dir() {
entry.path().to_path_buf()

View File

@@ -27,8 +27,10 @@ pub(crate) mod open;
mod pending;
pub(crate) mod rename;
pub(crate) mod save;
pub(crate) mod scan;
pub(crate) mod submit;
pub(crate) mod symlink;
pub(crate) mod walkdir;
pub(crate) mod watcher;
#[derive(Debug)]

View File

@@ -0,0 +1,19 @@
use std::path::Path;
use super::{File, FileTransferActivity};
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
impl FileTransferActivity {
pub(crate) fn action_scan(&mut self, p: &Path) -> Result<Vec<File>, String> {
match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => self
.host
.list_dir(p)
.map_err(|e| format!("Failed to list directory: {}", e)),
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self
.client
.list_dir(p)
.map_err(|e| format!("Failed to list directory: {}", e)),
}
}
}

View File

@@ -0,0 +1,97 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
// locals
use std::path::{Path, PathBuf};
use super::{File, FileTransferActivity};
use crate::ui::activities::filetransfer::lib::walkdir::WalkdirStates;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WalkdirError {
Aborted,
Error(String),
}
impl FileTransferActivity {
pub(crate) fn action_walkdir_local(&mut self) -> Result<Vec<File>, WalkdirError> {
let mut acc = Vec::with_capacity(32_768);
self.walkdir(&mut acc, &self.host.pwd(), |activity, path| {
activity.host.list_dir(path).map_err(|e| e.to_string())
})?;
Ok(acc)
}
pub(crate) fn action_walkdir_remote(&mut self) -> Result<Vec<File>, WalkdirError> {
let mut acc = Vec::with_capacity(32_768);
let pwd = self
.client
.pwd()
.map_err(|e| WalkdirError::Error(e.to_string()))?;
self.walkdir(&mut acc, &pwd, |activity, path| {
activity.client.list_dir(path).map_err(|e| e.to_string())
})?;
Ok(acc)
}
fn walkdir<F>(
&mut self,
acc: &mut Vec<File>,
path: &Path,
list_dir_fn: F,
) -> Result<(), WalkdirError>
where
F: Fn(&mut Self, &Path) -> Result<Vec<File>, String> + Copy,
{
// init acc if empty
if acc.is_empty() {
self.init_walkdir();
}
// list current directory
let dir_entries = list_dir_fn(self, path).map_err(WalkdirError::Error)?;
// get dirs to scan later
let dirs = dir_entries
.iter()
.filter(|entry| entry.is_dir())
.map(|entry| entry.path.clone())
.collect::<Vec<PathBuf>>();
// extend acc
acc.extend(dir_entries.clone());
// update view
self.update_walkdir_entries(acc.len());
// check aborted
self.check_aborted()?;
for dir in dirs {
self.walkdir(acc, &dir, list_dir_fn)?;
}
Ok(())
}
fn check_aborted(&mut self) -> Result<(), WalkdirError> {
// read events
self.tick();
// check if the user wants to abort
if self.walkdir.aborted {
return Err(WalkdirError::Aborted);
}
Ok(())
}
fn init_walkdir(&mut self) {
self.walkdir = WalkdirStates::default();
}
}

View File

@@ -5,8 +5,7 @@
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table};
use tuirealm::tui::layout::Corner;
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState};
use tuirealm::tui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue};
use super::{Msg, UiMsg};
@@ -63,7 +62,7 @@ impl MockComponent for Log {
focus,
None,
))
.start_corner(Corner::BottomLeft)
.direction(ListDirection::BottomToTop)
.highlight_symbol(">> ")
.style(Style::default().bg(bg))
.highlight_style(Style::default());

View File

@@ -17,12 +17,13 @@ mod transfer;
pub use misc::FooterBar;
pub use popups::{
ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup,
FileInfoPopup, FilterPopup, FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WatchedPathsList, WatcherPopup,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList,
WatcherPopup, ATTR_FILES,
};
pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote};
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
pub use self::log::Log;

View File

@@ -2,6 +2,9 @@
//!
//! popups components
mod chmod;
mod goto;
use std::time::UNIX_EPOCH;
use bytesize::ByteSize;
@@ -16,15 +19,13 @@ use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
#[cfg(unix)]
use users::{get_group_by_gid, get_user_by_uid};
pub use self::chmod::ChmodPopup;
pub use self::goto::{GotoPopup, ATTR_FILES};
use super::super::Browser;
use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
use crate::explorer::FileSorting;
use crate::utils::fmt::fmt_time;
mod chmod;
pub use chmod::ChmodPopup;
#[derive(MockComponent)]
pub struct CopyPopup {
component: Input,
@@ -131,7 +132,10 @@ impl FilterPopup {
"regex or wildmatch",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("Filter files by regex or wildmatch", Alignment::Center),
.title(
"Filter files by regex or wildmatch in the current directory",
Alignment::Center,
),
}
}
}
@@ -580,176 +584,6 @@ impl Component<Msg, NoUserEvent> for FileInfoPopup {
}
}
#[derive(MockComponent)]
pub struct FindPopup {
component: Input,
}
impl FindPopup {
pub fn new(color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.input_type(InputType::Text)
.placeholder(
"Search files by name",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("*.txt", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for FindPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
..
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.state() {
State::One(StateValue::String(i)) => {
Some(Msg::Transfer(TransferMsg::SearchFile(i)))
}
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseFindPopup))
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct GoToPopup {
component: Input,
}
impl GoToPopup {
pub fn new(color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.input_type(InputType::Text)
.placeholder(
"/foo/bar/buzz",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("Go to…", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for GoToPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
..
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.state() {
State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::GoTo(i))),
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseGotoPopup))
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct KeybindingsPopup {
component: List,
@@ -1675,6 +1509,7 @@ impl SortingPopup {
FileSorting::ModifyTime => 1,
FileSorting::Name => 0,
FileSorting::Size => 3,
FileSorting::None => 0,
}),
}
}
@@ -1778,6 +1613,7 @@ fn file_sorting_label(sorting: FileSorting) -> &'static str {
FileSorting::ModifyTime => "By modify time",
FileSorting::Name => "By name",
FileSorting::Size => "By size",
FileSorting::None => "",
}
}
@@ -1978,6 +1814,47 @@ impl Component<Msg, NoUserEvent> for WaitPopup {
}
}
#[derive(MockComponent)]
pub struct WalkdirWaitPopup {
component: Paragraph,
}
impl WalkdirWaitPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[
TextSpan::from(text.as_ref()),
TextSpan::from("Press 'CTRL+C' to abort"),
])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for WalkdirWaitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
if matches!(
ev,
Event::Keyboard(KeyEvent {
code: Key::Char('c'),
modifiers: KeyModifiers::CONTROL
})
) {
Some(Msg::Transfer(TransferMsg::AbortWalkdir))
} else {
None
}
}
}
#[derive(MockComponent)]
pub struct WatchedPathsList {
component: List,

View File

@@ -0,0 +1,435 @@
use std::path::PathBuf;
use tui_realm_stdlib::Input;
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
use tuirealm::{
AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent, State, StateValue,
};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
pub const ATTR_FILES: &str = "files";
#[derive(Default)]
struct OwnStates {
/// Path and name of the files
files: Vec<(String, String)>,
search: Option<String>,
last_suggestion: Option<String>,
}
impl OwnStates {
pub fn set_files(&mut self, files: Vec<String>) {
self.files = files
.into_iter()
.map(|f| {
(
f.clone(),
PathBuf::from(&f)
.file_name()
.map(|x| x.to_string_lossy().to_string())
.unwrap_or(f),
)
})
.collect();
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
enum Suggestion {
/// No suggestion
None,
/// Suggest a string
Suggest(String),
/// Rescan at `path` is required to satisfy the user input
Rescan(PathBuf),
}
impl From<CmdResult> for Suggestion {
fn from(value: CmdResult) -> Self {
match value {
CmdResult::Batch(v) if v.len() == 1 => {
if let CmdResult::Submit(State::One(StateValue::String(s))) = v.first().unwrap() {
Suggestion::Suggest(s.clone())
} else {
Suggestion::None
}
}
CmdResult::Batch(v) if v.len() == 2 => {
if let CmdResult::Submit(State::One(StateValue::String(s))) = v.get(1).unwrap() {
Suggestion::Rescan(PathBuf::from(s))
} else {
Suggestion::None
}
}
_ => Suggestion::None,
}
}
}
impl From<Suggestion> for CmdResult {
fn from(value: Suggestion) -> Self {
match value {
Suggestion::None => CmdResult::None,
Suggestion::Suggest(s) => {
CmdResult::Batch(vec![CmdResult::Submit(State::One(StateValue::String(s)))])
}
Suggestion::Rescan(p) => CmdResult::Batch(vec![
CmdResult::None,
CmdResult::Submit(State::One(StateValue::String(
p.to_string_lossy().to_string(),
))),
]),
}
}
}
impl OwnStates {
/// Return the current suggestion if any, otherwise return search
pub fn computed_search(&self) -> String {
match (&self.search, &self.last_suggestion) {
(_, Some(s)) => s.clone(),
(Some(s), _) => s.clone(),
_ => "".to_string(),
}
}
/// Suggest files based on the input
pub fn suggest(&mut self, input: &str) -> Suggestion {
debug!(
"Suggesting for: {input}; files {files:?}",
files = self.files
);
let is_path = PathBuf::from(input).is_absolute();
// case 1. search if any file starts with the input; get first if suggestion is `None`, otherwise get first after suggestion
let suggestions: Vec<&String> = self
.files
.iter()
.filter(|(path, file_name)| {
if is_path {
path.contains(input)
} else {
file_name.contains(input)
}
})
.map(|(path, _)| path)
.collect();
debug!("Suggestions for {input}: {:?}", suggestions);
// case 1. if suggestions not empty; then suggest next
if !suggestions.is_empty() {
let suggestion;
if let Some(last_suggestion) = self.last_suggestion.take() {
suggestion = suggestions
.iter()
.skip_while(|f| **f != &last_suggestion)
.nth(1)
.unwrap_or_else(|| suggestions.first().unwrap())
.to_string();
} else {
suggestion = suggestions.first().map(|x| x.to_string()).unwrap();
}
debug!("Suggested: {suggestion}");
self.last_suggestion = Some(suggestion.clone());
return Suggestion::Suggest(suggestion);
}
self.last_suggestion = None;
// case 2. otherwise convert suggest to a path and get the parent
// to rescan the files
let input_as_path = if input.starts_with('/') {
input.to_string()
} else {
format!("./{}", input)
};
let p = PathBuf::from(input_as_path);
let parent = p
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("/"));
// if path is `.`, then return None
if parent == PathBuf::from(".") {
return Suggestion::None;
}
debug!("Rescan required at: {}", parent.display());
Suggestion::Rescan(parent)
}
}
pub struct GotoPopup {
input: Input,
states: OwnStates,
}
impl GotoPopup {
pub fn new(color: Color, files: Vec<String>) -> Self {
let mut states = OwnStates::default();
states.set_files(files);
Self {
input: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.input_type(InputType::Text)
.placeholder(
"/foo/bar/buzz",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("Go to… (Press <TAB> for autocompletion)", Alignment::Center),
states,
}
}
}
impl MockComponent for GotoPopup {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::prelude::Rect) {
self.input.view(frame, area);
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
match attr {
Attribute::Custom(ATTR_FILES) => {
let files = value
.unwrap_payload()
.unwrap_vec()
.into_iter()
.map(|x| x.unwrap_str())
.collect();
self.states.set_files(files);
// call perform Change
self.perform(Cmd::Change);
}
_ => self.input.attr(attr, value),
}
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.input.query(attr)
}
fn state(&self) -> State {
State::One(StateValue::String(self.states.computed_search()))
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::Change => {
let input = self
.states
.search
.as_ref()
.cloned()
.unwrap_or_else(|| self.input.state().unwrap_one().unwrap_string());
let suggest = self.states.suggest(&input);
if let Suggestion::Suggest(suggestion) = suggest.clone() {
self.input
.attr(Attribute::Value, AttrValue::String(suggestion.clone()));
}
suggest.into()
}
cmd => {
let res = self.input.perform(cmd);
if let CmdResult::Changed(State::One(StateValue::String(new_text))) = &res {
self.states.search = Some(new_text.clone());
}
res
}
}
}
}
impl Component<Msg, NoUserEvent> for GotoPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
..
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
if let Suggestion::Rescan(path) = Suggestion::from(self.perform(Cmd::Change)) {
Some(Msg::Transfer(TransferMsg::RescanGotoFiles(path)))
} else {
Some(Msg::None)
}
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.state() {
State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::GoTo(i))),
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseGotoPopup))
}
_ => None,
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_should_convert_from_and_back_cmd_result() {
let s = Suggestion::Suggest("foo".to_string());
let cmd: CmdResult = s.clone().into();
let s2: Suggestion = cmd.into();
assert_eq!(s, s2);
let s = Suggestion::Rescan(PathBuf::from("/foo/bar"));
let cmd: CmdResult = s.clone().into();
let s2: Suggestion = cmd.into();
assert_eq!(s, s2);
}
#[test]
fn test_should_suggest_next() {
let mut states = OwnStates {
files: vec![
("/home/foo".to_string(), "foo".to_string()),
("/home/bar".to_string(), "bar".to_string()),
("/home/buzz".to_string(), "buzz".to_string()),
("/home/fizz".to_string(), "fizz".to_string()),
],
search: None,
last_suggestion: None,
};
let s = states.suggest("f");
assert_eq!(Suggestion::Suggest("/home/foo".to_string()), s);
let s = states.suggest("f");
assert_eq!(Suggestion::Suggest("/home/fizz".to_string()), s);
let s = states.suggest("f");
assert_eq!(Suggestion::Suggest("/home/foo".to_string()), s);
}
#[test]
#[cfg(unix)]
fn test_should_suggest_absolute_path() {
let mut states = OwnStates {
files: vec![
("/home/foo".to_string(), "foo".to_string()),
("/home/bar".to_string(), "bar".to_string()),
("/home/buzz".to_string(), "buzz".to_string()),
("/home/fizz".to_string(), "fizz".to_string()),
],
search: None,
last_suggestion: None,
};
let s = states.suggest("/home/f");
assert_eq!(Suggestion::Suggest("/home/foo".to_string()), s);
}
#[test]
fn test_should_suggest_rescan() {
let mut states = OwnStates {
files: vec![
("/home/foo".to_string(), "foo".to_string()),
("/home/bar".to_string(), "bar".to_string()),
("/home/buzz".to_string(), "buzz".to_string()),
("/home/fizz".to_string(), "fizz".to_string()),
],
search: None,
last_suggestion: None,
};
let s = states.suggest("/home/user");
assert_eq!(Suggestion::Rescan(PathBuf::from("/home")), s);
}
#[test]
fn test_should_suggest_none() {
let mut states = OwnStates {
files: vec![
("/home/foo".to_string(), "foo".to_string()),
("/home/bar".to_string(), "bar".to_string()),
("/home/buzz".to_string(), "buzz".to_string()),
("/home/fizz".to_string(), "fizz".to_string()),
],
search: None,
last_suggestion: None,
};
let s = states.suggest("");
assert_eq!(Suggestion::Suggest("/home/foo".to_string()), s);
}
#[test]
fn test_should_suggest_none_if_dot() {
let mut states = OwnStates {
files: vec![
("/home/foo".to_string(), "foo".to_string()),
("/home/bar".to_string(), "bar".to_string()),
("/home/buzz".to_string(), "buzz".to_string()),
("/home/fizz".to_string(), "fizz".to_string()),
],
search: None,
last_suggestion: None,
};
let s = states.suggest("./th");
assert_eq!(Suggestion::None, s);
}
}

View File

@@ -6,9 +6,8 @@ use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::props::{
Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers,
};
use tuirealm::tui::layout::Corner;
use tuirealm::tui::text::{Line, Span};
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState};
use tuirealm::tui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
use tuirealm::{MockComponent, Props, State, StateValue};
pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
@@ -235,7 +234,7 @@ impl MockComponent for FileList {
// Make list
let mut list = TuiList::new(list_items)
.block(div)
.start_corner(Corner::TopLeft);
.direction(ListDirection::TopToBottom);
if let Some(highlighted_color) = highlighted_color {
list = list.highlight_style(
Style::default()

View File

@@ -0,0 +1,160 @@
use tui_realm_stdlib::Input;
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Table};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::{MockComponent, State};
use super::file_list::FileList;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Focus {
List,
#[default]
Search,
}
#[derive(Default)]
struct OwnStates {
focus: Focus,
}
impl OwnStates {
pub fn next(&mut self) {
self.focus = match self.focus {
Focus::List => Focus::Search,
Focus::Search => Focus::List,
};
}
}
#[derive(Default)]
pub struct FileListWithSearch {
file_list: FileList,
search: Input,
states: OwnStates,
}
impl FileListWithSearch {
pub fn focus(&self) -> Focus {
self.states.focus
}
pub fn foreground(mut self, fg: Color) -> Self {
self.file_list
.attr(Attribute::Foreground, AttrValue::Color(fg));
self.search
.attr(Attribute::Foreground, AttrValue::Color(fg));
self
}
pub fn background(mut self, bg: Color) -> Self {
self.file_list
.attr(Attribute::Background, AttrValue::Color(bg));
self.search
.attr(Attribute::Background, AttrValue::Color(bg));
self
}
pub fn borders(mut self, b: Borders) -> Self {
self.file_list
.attr(Attribute::Borders, AttrValue::Borders(b.clone()));
self.search.attr(Attribute::Borders, AttrValue::Borders(b));
self
}
pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
self.file_list.attr(
Attribute::Title,
AttrValue::Title((t.as_ref().to_string(), a)),
);
self.search.attr(
Attribute::Title,
AttrValue::Title(("Fuzzy search".to_string(), a)),
);
self
}
pub fn highlighted_color(mut self, c: Color) -> Self {
self.file_list
.attr(Attribute::HighlightedColor, AttrValue::Color(c));
self
}
pub fn rows(mut self, rows: Table) -> Self {
self.file_list
.attr(Attribute::Content, AttrValue::Table(rows));
self
}
}
impl MockComponent for FileListWithSearch {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) {
// split the area in two
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // Search
Constraint::Fill(1), // File list
]
.as_ref(),
)
.split(area);
// render the search input
self.search.view(frame, chunks[0]);
// render the file list
self.file_list.view(frame, chunks[1]);
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.file_list.query(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
if attr == Attribute::Focus {
let value = value.unwrap_flag();
match value {
true => self.states.focus = Focus::Search,
false => self.states.focus = Focus::List,
}
self.search.attr(
Attribute::Focus,
AttrValue::Flag(self.states.focus == Focus::Search),
);
self.file_list.attr(
Attribute::Focus,
AttrValue::Flag(self.states.focus == Focus::List),
);
} else {
self.file_list.attr(attr, value);
}
}
fn state(&self) -> State {
match self.states.focus {
Focus::List => self.file_list.state(),
Focus::Search => self.search.state(),
}
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::Change => {
self.states.next();
self.search.attr(
Attribute::Focus,
AttrValue::Flag(self.states.focus == Focus::Search),
);
self.file_list.attr(
Attribute::Focus,
AttrValue::Flag(self.states.focus == Focus::List),
);
CmdResult::None
}
cmd if self.states.focus == Focus::Search => self.search.perform(cmd),
cmd => self.file_list.perform(cmd),
}
}
}

View File

@@ -2,14 +2,220 @@
//!
//! file transfer components
use super::{Msg, TransferMsg, UiMsg};
mod file_list;
use file_list::FileList;
use tuirealm::command::{Cmd, Direction, Position};
mod file_list_with_search;
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, Borders, Color, TextSpan};
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
use self::file_list::FileList;
use self::file_list_with_search::FileListWithSearch;
use super::{Msg, TransferMsg, UiMsg};
#[derive(MockComponent)]
pub struct ExplorerFuzzy {
component: FileListWithSearch,
}
impl ExplorerFuzzy {
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
Self {
component: FileListWithSearch::default()
.background(bg)
.borders(Borders::default().color(hg))
.foreground(fg)
.highlighted_color(hg)
.title(title, Alignment::Left)
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
}
}
fn on_search(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Tab | Key::Up | Key::Down,
..
}) => {
self.perform(Cmd::Change);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
..
}) => match self.perform(Cmd::Type(ch)) {
CmdResult::Changed(State::One(StateValue::String(search))) => {
Some(Msg::Ui(UiMsg::FuzzySearch(search)))
}
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseFindExplorer))
}
_ => None,
}
}
fn on_file_list(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char('a'),
modifiers: KeyModifiers::CONTROL,
}) => {
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char('a'),
modifiers: KeyModifiers::ALT,
}) => {
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_DESELECT_ALL));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char('m'),
modifiers: KeyModifiers::NONE,
}) => {
let _ = self.perform(Cmd::Toggle);
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
self.perform(Cmd::Change);
Some(Msg::None)
}
// -- comp msg
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseFindExplorer))
}
Event::Keyboard(KeyEvent {
code: Key::Left | Key::Right,
..
}) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)),
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)),
Event::Keyboard(KeyEvent {
code: Key::Char(' '),
..
}) => Some(Msg::Transfer(TransferMsg::TransferFile)),
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
Event::Keyboard(KeyEvent {
code: Key::Char('a'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)),
Event::Keyboard(KeyEvent {
code: Key::Char('b'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('e') | Key::Delete | Key::Function(8),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowDeletePopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('i'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('s') | Key::Function(2),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('v') | Key::Function(3),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Transfer(TransferMsg::OpenFile)),
Event::Keyboard(KeyEvent {
code: Key::Char('w'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('z'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowChmodPopup)),
_ => None,
}
}
}
impl Component<Msg, NoUserEvent> for ExplorerFuzzy {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match self.component.focus() {
file_list_with_search::Focus::List => self.on_file_list(ev),
file_list_with_search::Focus::Search => self.on_search(ev),
}
}
}
#[derive(MockComponent)]
pub struct ExplorerFind {
@@ -261,7 +467,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
Event::Keyboard(KeyEvent {
code: Key::Char('f'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
}) => Some(Msg::Transfer(TransferMsg::InitFuzzySearch)),
Event::Keyboard(KeyEvent {
code: Key::Char('g'),
modifiers: KeyModifiers::NONE,
@@ -457,7 +663,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
Event::Keyboard(KeyEvent {
code: Key::Char('f'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
}) => Some(Msg::Transfer(TransferMsg::InitFuzzySearch)),
Event::Keyboard(KeyEvent {
code: Key::Char('g'),
modifiers: KeyModifiers::NONE,

View File

@@ -4,12 +4,15 @@
use std::path::Path;
use nucleo::Utf32String;
use remotefs::File;
use crate::explorer::builder::FileExplorerBuilder;
use crate::explorer::{FileExplorer, FileSorting, GroupDirs};
use crate::explorer::{FileExplorer, FileSorting};
use crate::system::config_client::ConfigClient;
const FUZZY_SEARCH_THRESHOLD: u16 = 50;
/// File explorer tab
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum FileExplorerTab {
@@ -28,10 +31,10 @@ pub enum FoundExplorerTab {
/// Browser contains the browser options
pub struct Browser {
local: FileExplorer, // Local File explorer state
remote: FileExplorer, // Remote File explorer state
found: Option<(FoundExplorerTab, FileExplorer)>, // File explorer for find result
tab: FileExplorerTab, // Current selected tab
local: FileExplorer, // Local File explorer state
remote: FileExplorer, // Remote File explorer state
found: Option<Found>, // File explorer for find result
tab: FileExplorerTab, // Current selected tab
pub sync_browsing: bool,
}
@@ -47,6 +50,16 @@ impl Browser {
}
}
pub fn explorer(&self) -> &FileExplorer {
match self.tab {
FileExplorerTab::Local => &self.local,
FileExplorerTab::Remote => &self.remote,
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
self.found.as_ref().map(|x| &x.explorer).unwrap()
}
}
}
pub fn local(&self) -> &FileExplorer {
&self.local
}
@@ -64,17 +77,35 @@ impl Browser {
}
pub fn found(&self) -> Option<&FileExplorer> {
self.found.as_ref().map(|x| &x.1)
self.found.as_ref().map(|x| &x.explorer)
}
pub fn found_mut(&mut self) -> Option<&mut FileExplorer> {
self.found.as_mut().map(|x| &mut x.1)
self.found.as_mut().map(|x| &mut x.explorer)
}
/// Perform fuzzy search on found tab
pub fn fuzzy_search(&mut self, needle: &str) {
if let Some(x) = self.found.as_mut() {
x.fuzzy_search(needle)
}
}
/// Initialize fuzzy search
pub fn init_fuzzy_search(&mut self) {
if let Some(explorer) = self.found_mut() {
explorer.set_files(vec![]);
}
}
pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec<File>, wrkdir: &Path) {
let mut explorer = Self::build_found_explorer(wrkdir);
explorer.set_files(files);
self.found = Some((tab, explorer));
explorer.set_files(files.clone());
self.found = Some(Found {
tab,
explorer,
search_results: files,
});
}
pub fn del_found(&mut self) {
@@ -83,7 +114,7 @@ impl Browser {
/// Returns found tab if any
pub fn found_tab(&self) -> Option<FoundExplorerTab> {
self.found.as_ref().map(|x| x.0)
self.found.as_ref().map(|x| x.tab)
}
pub fn tab(&self) -> FileExplorerTab {
@@ -129,8 +160,8 @@ impl Browser {
/// Build explorer reading from `ConfigClient`, for found result (has some differences)
fn build_found_explorer(wrkdir: &Path) -> FileExplorer {
FileExplorerBuilder::new()
.with_file_sorting(FileSorting::Name)
.with_group_dirs(Some(GroupDirs::First))
.with_file_sorting(FileSorting::None)
.with_group_dirs(None)
.with_hidden_files(true)
.with_stack_size(0)
.with_formatter(Some(
@@ -139,3 +170,48 @@ impl Browser {
.build()
}
}
/// Found state
struct Found {
explorer: FileExplorer,
/// Search results; original copy of files
search_results: Vec<File>,
tab: FoundExplorerTab,
}
impl Found {
/// Fuzzy search from `search_results` and update `explorer.files` with the results.
pub fn fuzzy_search(&mut self, needle: &str) {
let search = Utf32String::from(needle);
let mut nucleo = nucleo::Matcher::new(nucleo::Config::DEFAULT.match_paths());
// get scores
let mut fuzzy_results_with_score = self
.search_results
.iter()
.map(|f| {
(
Utf32String::from(f.path().to_string_lossy().into_owned()),
f,
)
})
.filter_map(|(path, file)| {
nucleo
.fuzzy_match(path.slice(..), search.slice(..))
.map(|score| (path, file, score))
})
.filter(|(_, _, score)| *score >= FUZZY_SEARCH_THRESHOLD)
.collect::<Vec<_>>();
// sort by score; highest first
fuzzy_results_with_score.sort_by(|(_, _, a), (_, _, b)| b.cmp(a));
// update files
self.explorer.set_files(
fuzzy_results_with_score
.into_iter()
.map(|(_, file, _)| file.clone())
.collect(),
);
}
}

View File

@@ -4,3 +4,4 @@
pub(crate) mod browser;
pub(crate) mod transfer;
pub(crate) mod walkdir;

View File

@@ -0,0 +1,4 @@
#[derive(Debug, Default)]
pub struct WalkdirStates {
pub aborted: bool,
}

View File

@@ -22,7 +22,7 @@ const LOG_CAPACITY: usize = 256;
impl FileTransferActivity {
/// Call `Application::tick()` and process messages in `Update`
pub(super) fn tick(&mut self) {
match self.app.tick(PollStrategy::UpTo(3)) {
match self.app.tick(PollStrategy::UpTo(1)) {
Ok(messages) => {
if !messages.is_empty() {
self.redraw = true;
@@ -111,7 +111,9 @@ impl FileTransferActivity {
match &ft_params.params {
ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
ProtocolParams::Kube(params) => params.pod.clone(),
ProtocolParams::Kube(params) => {
params.namespace.clone().unwrap_or("default".to_string())
}
ProtocolParams::Smb(params) => params.address.clone(),
ProtocolParams::WebDAV(params) => params.uri.clone(),
}
@@ -137,11 +139,9 @@ impl FileTransferActivity {
format!("Connecting to {}", params.bucket_name)
}
ProtocolParams::Kube(params) => {
info!(
"Client is not connected to remote; connecting to pod {}",
params.pod,
);
format!("Connecting to {}", params.pod)
let namespace = params.namespace.as_deref().unwrap_or("default");
info!("Client is not connected to remote; connecting to namespace {namespace}",);
format!("Connecting to Kube namespace {namespace}",)
}
ProtocolParams::Smb(params) => {
info!(

View File

@@ -14,6 +14,7 @@ mod view;
// locals
use std::collections::VecDeque;
use std::path::PathBuf;
use std::time::Duration;
// Includes
@@ -21,6 +22,7 @@ use chrono::{DateTime, Local};
use lib::browser;
use lib::browser::Browser;
use lib::transfer::{TransferOpts, TransferStates};
use lib::walkdir::WalkdirStates;
use remotefs::RemoteFs;
use session::TransferPayload;
use tempfile::TempDir;
@@ -50,7 +52,6 @@ enum Id {
FatalPopup,
FileInfoPopup,
FilterPopup,
FindPopup,
FooterBar,
GlobalListener,
GotoPopup,
@@ -94,6 +95,7 @@ enum PendingActionMsg {
#[derive(Debug, PartialEq)]
enum TransferMsg {
AbortWalkdir,
AbortTransfer,
Chmod(remotefs::fs::UnixPex),
CopyFileTo(String),
@@ -104,6 +106,7 @@ enum TransferMsg {
GoTo(String),
GoToParentDirectory,
GoToPreviousDirectory,
InitFuzzySearch,
Mkdir(String),
NewFile(String),
OpenFile,
@@ -111,8 +114,8 @@ enum TransferMsg {
OpenTextFile,
ReloadDir,
RenameFile(String),
RescanGotoFiles(PathBuf),
SaveFileAs(String),
SearchFile(String),
ToggleWatch,
ToggleWatchFor(usize),
TransferFile,
@@ -133,7 +136,6 @@ enum UiMsg {
CloseFileSortingPopup,
CloseFilterPopup,
CloseFindExplorer,
CloseFindPopup,
CloseGotoPopup,
CloseKeybindingsPopup,
CloseMkdirPopup,
@@ -147,6 +149,7 @@ enum UiMsg {
CloseWatcherPopup,
Disconnect,
FilterFiles(String),
FuzzySearch(String),
LogBackTabbed,
Quit,
ReplacePopupTabbed,
@@ -158,7 +161,6 @@ enum UiMsg {
ShowFileInfoPopup,
ShowFileSortingPopup,
ShowFilterPopup,
ShowFindPopup,
ShowGotoPopup,
ShowKeybindingsPopup,
ShowLogPanel,
@@ -219,6 +221,9 @@ pub struct FileTransferActivity {
browser: Browser,
/// Current log lines
log_records: VecDeque<LogRecord>,
/// Fuzzy search states
walkdir: WalkdirStates,
/// Transfer states
transfer: TransferStates,
/// Temporary directory where to store temporary stuff
cache: Option<TempDir>,
@@ -246,6 +251,7 @@ impl FileTransferActivity {
client: Builder::build(params.protocol, params.params.clone(), &config_client),
browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
walkdir: WalkdirStates::default(),
transfer: TransferStates::default(),
cache: match TempDir::new() {
Ok(d) => Some(d),

View File

@@ -57,7 +57,11 @@ impl FileTransferActivity {
// Connect to remote
match self.client.connect() {
Ok(Welcome { banner, .. }) => {
self.connected = true;
self.connected = self.client.is_connected();
if !self.connected {
return;
}
if let Some(banner) = banner {
// Log welcome
self.log(
@@ -68,6 +72,15 @@ impl FileTransferActivity {
banner
),
);
} else {
// Log welcome
self.log(
LogLevel::Info,
format!(
"Established connection with '{}'",
self.get_remote_hostname()
),
);
}
// Try to change directory to entry directory
let mut remote_chdir: Option<PathBuf> = None;
@@ -87,7 +100,7 @@ impl FileTransferActivity {
Err(err) => {
// Set popup fatal error
self.umount_wait();
self.mount_fatal(&err.to_string());
self.mount_fatal(err.to_string());
}
}
}
@@ -111,16 +124,28 @@ impl FileTransferActivity {
/// Reload remote directory entries and update browser
pub(super) fn reload_remote_dir(&mut self) {
if !self.connected {
return;
}
// Get current entries
if let Ok(wrkdir) = self.client.pwd() {
self.mount_blocking_wait("Loading remote directory...");
if self.remote_scan(wrkdir.as_path()).is_ok() {
// Set wrkdir
self.remote_mut().wrkdir = wrkdir;
}
let res = self.remote_scan(wrkdir.as_path());
self.umount_wait();
match res {
Ok(_) => {
self.remote_mut().wrkdir = wrkdir;
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not scan current remote directory: {err}"),
);
}
}
}
}
@@ -130,29 +155,33 @@ impl FileTransferActivity {
let wrkdir: PathBuf = self.host.pwd();
if self.local_scan(wrkdir.as_path()).is_ok() {
self.local_mut().wrkdir = wrkdir;
}
let res = self.local_scan(wrkdir.as_path());
self.umount_wait();
match res {
Ok(_) => {
self.local_mut().wrkdir = wrkdir;
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not scan current local directory: {err}"),
);
}
}
}
/// Scan current local directory
fn local_scan(&mut self, path: &Path) -> Result<(), HostError> {
match self.host.scan_dir(path) {
match self.host.list_dir(path) {
Ok(files) => {
// Set files and sort (sorting is implicit)
self.local_mut().set_files(files);
Ok(())
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not scan current directory: {err}"),
);
Err(err)
}
Err(err) => Err(err),
}
}
@@ -164,13 +193,7 @@ impl FileTransferActivity {
self.remote_mut().set_files(files);
Ok(())
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not scan current directory: {err}"),
);
Err(err)
}
Err(err) => Err(err),
}
}
@@ -335,7 +358,7 @@ impl FileTransferActivity {
}
}
// Get files in dir
match self.host.scan_dir(entry.path()) {
match self.host.list_dir(entry.path()) {
Ok(entries) => {
// Iterate over files
for entry in entries.iter() {
@@ -1133,7 +1156,7 @@ impl FileTransferActivity {
fn get_total_transfer_size_local(&mut self, entry: &File) -> usize {
if entry.is_dir() {
// List dir
match self.host.scan_dir(entry.path()) {
match self.host.list_dir(entry.path()) {
Ok(files) => files
.iter()
.map(|x| self.get_total_transfer_size_local(x))

View File

@@ -8,6 +8,7 @@ use remotefs::fs::File;
use tuirealm::props::{AttrValue, Attribute};
use tuirealm::{State, StateValue, Update};
use super::actions::walkdir::WalkdirError;
use super::actions::SelectedFile;
use super::browser::{FileExplorerTab, FoundExplorerTab};
use super::{ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg};
@@ -32,6 +33,9 @@ impl FileTransferActivity {
TransferMsg::AbortTransfer => {
self.transfer.abort();
}
TransferMsg::AbortWalkdir => {
self.walkdir.aborted = true;
}
TransferMsg::Chmod(mode) => {
self.umount_chmod();
self.mount_blocking_wait("Applying new file mode…");
@@ -211,6 +215,59 @@ impl FileTransferActivity {
_ => {}
}
}
TransferMsg::InitFuzzySearch => {
// Mount wait
self.mount_walkdir_wait();
// Find
let res: Result<Vec<File>, WalkdirError> = match self.browser.tab() {
FileExplorerTab::Local => self.action_walkdir_local(),
FileExplorerTab::Remote => self.action_walkdir_remote(),
_ => panic!("Trying to search for files, while already in a find result"),
};
// Umount wait
self.umount_wait();
// Match result
match res {
Err(WalkdirError::Error(err)) => {
// Mount error
self.mount_error(err.as_str());
}
Err(WalkdirError::Aborted) => {
self.mount_info("Search aborted");
}
Ok(files) if files.is_empty() => {
// If no file has been found notify user
self.mount_info("There are no files in the current directory");
}
Ok(files) => {
// Get wrkdir
let wrkdir = match self.browser.tab() {
FileExplorerTab::Local => self.local().wrkdir.clone(),
_ => self.remote().wrkdir.clone(),
};
// Create explorer and load files
self.browser.set_found(
match self.browser.tab() {
FileExplorerTab::Local => FoundExplorerTab::Local,
_ => FoundExplorerTab::Remote,
},
files,
wrkdir.as_path(),
);
// init fuzzy search to display nothing
self.browser.init_fuzzy_search();
// Mount result widget
self.mount_find(format!(r#"Searching at "{}""#, wrkdir.display()), true);
self.update_find_list();
// Initialize tab
self.browser.change_tab(match self.browser.tab() {
FileExplorerTab::Local => FileExplorerTab::FindLocal,
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
_ => FileExplorerTab::FindLocal,
});
}
}
}
TransferMsg::Mkdir(dir) => {
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_mkdir(dir),
@@ -267,6 +324,15 @@ impl FileTransferActivity {
// Reload files
self.update_browser_file_list()
}
TransferMsg::RescanGotoFiles(path) => {
let files = self.action_scan(&path).unwrap_or_default();
let files = files
.into_iter()
.filter(|f| f.is_dir() || f.is_symlink())
.map(|f| f.path().to_string_lossy().to_string())
.collect();
self.update_goto(files);
}
TransferMsg::SaveFileAs(dest) => {
self.umount_saveas();
match self.browser.tab() {
@@ -281,57 +347,7 @@ impl FileTransferActivity {
// Reload files
self.update_browser_file_list_swapped();
}
TransferMsg::SearchFile(search) => {
self.umount_find_input();
// Mount wait
self.mount_blocking_wait(format!(r#"Searching for "{search}"…"#).as_str());
// Find
let res: Result<Vec<File>, String> = match self.browser.tab() {
FileExplorerTab::Local => self.action_local_find(search.clone()),
FileExplorerTab::Remote => self.action_remote_find(search.clone()),
_ => panic!("Trying to search for files, while already in a find result"),
};
// Umount wait
self.umount_wait();
// Match result
match res {
Err(err) => {
// Mount error
self.mount_error(err.as_str());
}
Ok(files) if files.is_empty() => {
// If no file has been found notify user
self.mount_info(
format!(r#"Could not find any file matching "{search}""#).as_str(),
);
}
Ok(files) => {
// Get wrkdir
let wrkdir = match self.browser.tab() {
FileExplorerTab::Local => self.local().wrkdir.clone(),
_ => self.remote().wrkdir.clone(),
};
// Create explorer and load files
self.browser.set_found(
match self.browser.tab() {
FileExplorerTab::Local => FoundExplorerTab::Local,
_ => FoundExplorerTab::Remote,
},
files,
wrkdir.as_path(),
);
// Mount result widget
self.mount_find(&search);
self.update_find_list();
// Initialize tab
self.browser.change_tab(match self.browser.tab() {
FileExplorerTab::Local => FileExplorerTab::FindLocal,
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
_ => FileExplorerTab::FindLocal,
});
}
}
}
TransferMsg::ToggleWatch => self.action_toggle_watch(),
TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index),
TransferMsg::TransferFile => {
@@ -405,7 +421,6 @@ impl FileTransferActivity {
self.finalize_find();
self.umount_find();
}
UiMsg::CloseFindPopup => self.umount_find_input(),
UiMsg::CloseGotoPopup => self.umount_goto(),
UiMsg::CloseKeybindingsPopup => self.umount_help(),
UiMsg::CloseMkdirPopup => self.umount_mkdir(),
@@ -439,7 +454,7 @@ impl FileTransferActivity {
wrkdir.as_path(),
);
// Mount result widget
self.mount_find(&filter);
self.mount_find(&filter, false);
self.update_find_list();
// Initialize tab
self.browser.change_tab(match self.browser.tab() {
@@ -448,6 +463,10 @@ impl FileTransferActivity {
_ => FileExplorerTab::FindLocal,
});
}
UiMsg::FuzzySearch(needle) => {
self.browser.fuzzy_search(&needle);
self.update_find_list();
}
UiMsg::ShowLogPanel => {
assert!(self.app.active(&Id::Log).is_ok());
}
@@ -514,7 +533,6 @@ impl FileTransferActivity {
}
UiMsg::ShowFileSortingPopup => self.mount_file_sorting(),
UiMsg::ShowFilterPopup => self.mount_filter(),
UiMsg::ShowFindPopup => self.mount_find_input(),
UiMsg::ShowGotoPopup => self.mount_goto(),
UiMsg::ShowKeybindingsPopup => self.mount_help(),
UiMsg::ShowMkdirPopup => self.mount_mkdir(),

View File

@@ -6,12 +6,14 @@
// Ext
use remotefs::fs::{File, UnixPex};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{PropPayload, PropValue, TextSpan};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::tui::widgets::Clear;
use tuirealm::{Sub, SubClause, SubEventClause};
use tuirealm::{AttrValue, Attribute, Sub, SubClause, SubEventClause};
use unicode_width::UnicodeWidthStr;
use super::browser::{FileExplorerTab, FoundExplorerTab};
use super::components::ATTR_FILES;
use super::{components, Context, FileTransferActivity, Id};
use crate::explorer::FileSorting;
use crate::utils::ui::{Popup, Size};
@@ -80,7 +82,7 @@ impl FileTransferActivity {
self.refresh_remote_status_bar();
// Update components
self.update_local_filelist();
self.update_remote_filelist();
// self.update_remote_filelist();
// Global listener
self.mount_global_listener();
// Give focus to local explorer
@@ -177,11 +179,6 @@ impl FileTransferActivity {
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::FilterPopup, f, popup);
} else if self.app.mounted(&Id::FindPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size());
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::FindPopup, f, popup);
} else if self.app.mounted(&Id::GotoPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size());
f.render_widget(Clear, popup);
@@ -307,7 +304,15 @@ impl FileTransferActivity {
// make popup
self.app.view(&Id::ErrorPopup, f, popup);
} else if self.app.mounted(&Id::WaitPopup) {
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.size());
let wait_popup_lines = self
.app
.query(&Id::WaitPopup, Attribute::Text)
.map(|x| x.map(|x| x.unwrap_payload().unwrap_vec().len()))
.unwrap_or_default()
.unwrap_or(1) as u16;
let popup =
Popup(Size::Percentage(50), Size::Unit(2 + wait_popup_lines)).draw_in(f.size());
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::WaitPopup, f, popup);
@@ -397,6 +402,38 @@ impl FileTransferActivity {
assert!(self.app.active(&Id::WaitPopup).is_ok());
}
pub(super) fn mount_walkdir_wait(&mut self) {
let color = self.theme().misc_info_dialog;
assert!(self
.app
.remount(
Id::WaitPopup,
Box::new(components::WalkdirWaitPopup::new(
"Scanning current directory…",
color
)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::WaitPopup).is_ok());
self.view();
}
pub(super) fn update_walkdir_entries(&mut self, entries: usize) {
let text = format!("Scanning current directory… ({entries} items found)",);
let _ = self.app.attr(
&Id::WaitPopup,
Attribute::Text,
AttrValue::Payload(PropPayload::Vec(vec![
PropValue::TextSpan(TextSpan::from(text)),
PropValue::TextSpan(TextSpan::from("Press 'CTRL+C' to abort")),
])),
);
self.view();
}
pub(super) fn mount_blocking_wait<S: AsRef<str>>(&mut self, text: S) {
self.mount_wait(text);
self.view();
@@ -515,7 +552,7 @@ impl FileTransferActivity {
let _ = self.app.umount(&Id::ExecPopup);
}
pub(super) fn mount_find(&mut self, search: &str) {
pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) {
// Get color
let (bg, fg, hg) = match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => (
@@ -529,18 +566,29 @@ impl FileTransferActivity {
self.theme().transfer_remote_explorer_highlighted,
),
};
// Mount component
assert!(self
.app
.remount(
Id::ExplorerFind,
Box::new(components::ExplorerFind::new(
format!(r#"Search results for "{search}""#),
&[],
bg,
fg,
hg
)),
if fuzzy_search {
Box::new(components::ExplorerFuzzy::new(
msg.to_string(),
&[],
bg,
fg,
hg,
))
} else {
Box::new(components::ExplorerFind::new(
msg.to_string(),
&[],
bg,
fg,
hg,
))
},
vec![],
)
.is_ok());
@@ -551,37 +599,41 @@ impl FileTransferActivity {
let _ = self.app.umount(&Id::ExplorerFind);
}
pub(super) fn mount_find_input(&mut self) {
let input_color = self.theme().misc_input_dialog;
assert!(self
.app
.remount(
Id::FindPopup,
Box::new(components::FindPopup::new(input_color)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::FindPopup).is_ok());
}
pub(super) fn umount_find_input(&mut self) {
// Umount input find
let _ = self.app.umount(&Id::FindPopup);
}
pub(super) fn mount_goto(&mut self) {
// get files
let files = self
.browser
.explorer()
.iter_files()
.filter(|f| f.is_dir() || f.is_symlink())
.map(|f| f.path().to_string_lossy().to_string())
.collect::<Vec<String>>();
let input_color = self.theme().misc_input_dialog;
assert!(self
.app
.remount(
Id::GotoPopup,
Box::new(components::GoToPopup::new(input_color)),
Box::new(components::GotoPopup::new(input_color, files)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::GotoPopup).is_ok());
}
pub(super) fn update_goto(&mut self, files: Vec<String>) {
let payload = files
.into_iter()
.map(PropValue::Str)
.collect::<Vec<PropValue>>();
let _ = self.app.attr(
&Id::GotoPopup,
Attribute::Custom(ATTR_FILES),
AttrValue::Payload(PropPayload::Vec(payload)),
);
}
pub(super) fn umount_goto(&mut self) {
let _ = self.app.umount(&Id::GotoPopup);
}
@@ -1094,38 +1146,33 @@ impl FileTransferActivity {
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SortingPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::FindPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SyncBrowsingMkdirPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SymlinkPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SymlinkPopup,
Id::WatcherPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WatcherPopup,
Id::WatchedPathsList,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WatchedPathsList,
Id::ChmodPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::ChmodPopup,
Id::WaitPopup,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::FilterPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WaitPopup,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::FilterPopup,
)))),
)),
)),
)),
)),

View File

@@ -124,7 +124,7 @@ impl SetupActivity {
}
/// Create a new ssh key
pub(super) fn action_new_ssh_key(&mut self) {
pub(super) fn action_new_ssh_key(&mut self) -> Result<(), String> {
// get parameters
let host: String = match self.app.state(&Id::Ssh(IdSsh::SshHost)) {
Ok(State::One(StateValue::String(host))) => host,
@@ -148,29 +148,23 @@ impl SetupActivity {
// Lock ports
assert!(self.app.lock_ports().is_ok());
// Write key to file
match edit::edit(placeholder.as_bytes()) {
let res = match edit::edit(placeholder.as_bytes()) {
Ok(rsa_key) => {
// Remove placeholder from `rsa_key`
let rsa_key: String = rsa_key.as_str().replace(placeholder.as_str(), "");
if rsa_key.is_empty() {
// Report error: empty key
self.mount_error("SSH key is empty!");
Err("SSH key is empty!".to_string())
} else {
// Add key
if let Err(err) =
self.add_ssh_key(host.as_str(), username.as_str(), rsa_key.as_str())
{
self.mount_error(
format!("Could not create new private key: {err}").as_str(),
);
}
self.add_ssh_key(host.as_str(), username.as_str(), rsa_key.as_str())
.map_err(|e| format!("Could not create new private key: {e}"))
}
}
Err(err) => {
// Report error
self.mount_error(format!("Could not write private key to file: {err}").as_str());
Err(format!("Could not write private key to file: {err}"))
}
}
};
// Restore terminal
if let Some(ctx) = self.context.as_mut() {
// Enter alternate mode
@@ -187,6 +181,8 @@ impl SetupActivity {
// Unlock ports
assert!(self.app.unlock_ports().is_ok());
}
res
}
/// Given a component and a color, save the color into the theme

View File

@@ -225,9 +225,14 @@ impl SetupActivity {
}
}
SshMsg::SaveSshKey => {
self.action_new_ssh_key();
let res = self.action_new_ssh_key();
self.umount_new_ssh_key();
self.reload_ssh_keys();
match res {
Ok(_) => {
self.reload_ssh_keys();
}
Err(err) => self.mount_error(&err),
}
}
SshMsg::ShowDelSshKeyPopup => {
self.mount_del_ssh_key();

View File

@@ -30,7 +30,7 @@ impl Context {
theme_provider: ThemeProvider,
error: Option<String>,
) -> Context {
Context {
let mut ctx = Context {
bookmarks_client,
config_client,
ft_params: None,
@@ -38,7 +38,13 @@ impl Context {
terminal: TerminalBridge::new().expect("Could not initialize terminal"),
theme_provider,
error,
}
};
// Init terminal state
let _ = ctx.terminal.enable_raw_mode();
let _ = ctx.terminal.enter_alternate_screen();
ctx
}
// -- getters
@@ -107,6 +113,5 @@ impl Drop for Context {
// Re-enable terminal stuff
let _ = self.terminal.disable_raw_mode();
let _ = self.terminal.leave_alternate_screen();
let _ = self.terminal.clear_screen();
}
}

View File

@@ -56,12 +56,12 @@ static REMOTE_WEBDAV_OPT_REGEX: Lazy<Regex> =
lazy_regex!(r"(?:([^:]+):)(?:(.+[^@])@)(?:([^/]+))(?:(.+))?");
/**
* Regex matches: {container}@{pod}/{path}
* - group 1: Container
* - group 2: Pod
* - group 3: Some(path) | None
* Regex matches: {namespace}[@{cluster_url}]$/{path}
* - group 1: Namespace
* - group 3: Some(cluster_url) | None
* - group 5: Some(path) | None
*/
static REMOTE_KUBE_OPT_REGEX: Lazy<Regex> = lazy_regex!(r"(?:(.+[^@])@)(?:([^/]+))(?:(.+))?");
static REMOTE_KUBE_OPT_REGEX: Lazy<Regex> = lazy_regex!(r"(?:([^@]+))(@(?:([^$]+)))?(\$(?:(.+)))?");
/**
* Regex matches:
@@ -100,8 +100,7 @@ static REMOTE_SMB_OPT_REGEX: Lazy<Regex> =
/**
* Regex matches:
* - group 1: Version
* E.g. termscp-0.3.2 => 0.3.2
* v0.4.0 => 0.4.0
* E.g. termscp-0.3.2 => 0.3.2; v0.4.0 => 0.4.0
*/
static SEMVER_REGEX: Lazy<Regex> = lazy_regex!(r".*(:?[0-9]\.[0-9]\.[0-9])");
@@ -314,23 +313,15 @@ fn parse_s3_remote_opt(s: &str) -> Result<FileTransferParams, String> {
fn parse_kube_remote_opt(s: &str) -> Result<FileTransferParams, String> {
match REMOTE_KUBE_OPT_REGEX.captures(s) {
Some(groups) => {
let container: String = groups
.get(1)
.map(|x| x.as_str().to_string())
.unwrap_or_default();
let pod: String = groups
.get(2)
.map(|x| x.as_str().to_string())
.unwrap_or_default();
let namespace: Option<String> = groups.get(1).map(|x| x.as_str().to_string());
let cluster_url: Option<String> = groups.get(3).map(|x| x.as_str().to_string());
let remote_path: Option<PathBuf> =
groups.get(3).map(|group| PathBuf::from(group.as_str()));
groups.get(5).map(|group| PathBuf::from(group.as_str()));
Ok(FileTransferParams::new(
FileTransferProtocol::Kube,
ProtocolParams::Kube(KubeProtocolParams {
pod,
container,
namespace: None,
cluster_url: None,
namespace,
cluster_url,
username: None,
client_cert: None,
client_key: None,
@@ -754,13 +745,13 @@ mod tests {
#[test]
fn should_parse_kube_address() {
let result = parse_remote_opt("kube://alpine@my-pod/tmp").ok().unwrap();
let result = parse_remote_opt("kube://my-namespace@http://localhost:1234$/tmp")
.ok()
.unwrap();
let params = result.params.kube_params().unwrap();
assert_eq!(params.container.as_str(), "alpine");
assert_eq!(params.pod.as_str(), "my-pod");
assert_eq!(params.namespace, None);
assert_eq!(params.cluster_url, None);
assert_eq!(params.namespace, Some("my-namespace".to_string()));
assert_eq!(params.cluster_url.as_deref(), Some("http://localhost:1234"));
assert_eq!(params.username, None);
assert_eq!(params.client_cert, None);
assert_eq!(params.client_key, None);