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
+5 -5
View File
@@ -22,21 +22,21 @@ jobs:
runs-on: ${{ matrix.platform.os }} runs-on: ${{ matrix.platform.os }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: stable toolchain: stable
override: true targets: ${{ matrix.platform.target }}
target: ${{ matrix.platform.target }}
- name: Build release - name: Build release
run: cargo build --release --target ${{ matrix.platform.target }} run: cargo build --release --target ${{ matrix.platform.target }}
- name: Prepare artifact files - name: Prepare artifact files
run: | run: |
mkdir -p .artifact mkdir -p .artifact
mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp
ls -l .artifact/
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
if-no-files-found: error if-no-files-found: error
retention-days: 1 retention-days: 1
name: ${{ matrix.platform.release_for }} name: ${{ matrix.platform.release_for }}
path: .artifact/* path: .artifact/termscp
-45
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 }}
+1 -2
View File
@@ -21,10 +21,9 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: stable toolchain: stable
override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Run tests - name: Run tests
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
+1 -2
View File
@@ -18,10 +18,9 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: stable toolchain: stable
override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Build - name: Build
run: cargo build run: cargo build
+1 -2
View File
@@ -19,10 +19,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: stable toolchain: stable
override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Build - name: Build
run: cargo build run: cargo build
+15
View File
@@ -1,6 +1,7 @@
# Changelog # Changelog
- [Changelog](#changelog) - [Changelog](#changelog)
- [0.15.0](#0150)
- [0.14.0](#0140) - [0.14.0](#0140)
- [0.13.0](#0130) - [0.13.0](#0130)
- [0.12.3](#0123) - [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 ## 0.14.0
Released on 17/07/2024 Released on 17/07/2024
Generated
+1056 -1488
View File
File diff suppressed because it is too large Load Diff
+33 -33
View File
@@ -5,18 +5,12 @@ description = "termscp is a feature rich terminal file transfer and explorer wit
edition = "2021" edition = "2021"
homepage = "https://termscp.veeso.dev" homepage = "https://termscp.veeso.dev"
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
keywords = [ keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
"scp-client",
"sftp-client",
"ftp-client",
"winscp",
"command-line-utility",
]
license = "MIT" license = "MIT"
name = "termscp" name = "termscp"
readme = "README.md" readme = "README.md"
repository = "https://github.com/veeso/termscp" repository = "https://github.com/veeso/termscp"
version = "0.14.0" version = "0.15.0"
[package.metadata.rpm] [package.metadata.rpm]
package = "termscp" package = "termscp"
@@ -38,32 +32,37 @@ path = "src/main.rs"
[dependencies] [dependencies]
argh = "^0.1" argh = "^0.1"
bitflags = "^2.1" bitflags = "^2"
bytesize = "^1.1" bytesize = "^1"
chrono = "^0.4" chrono = "^0.4"
content_inspector = "^0.2" content_inspector = "^0.2"
dirs = "^5.0" dirs = "^5.0"
edit = "^0.1" edit = "^0.1"
filetime = "^0.2" filetime = "^0.2"
hostname = "^0.4" hostname = "^0.4"
keyring = { version = "^2.0", optional = true } keyring = { version = "^3", optional = true, features = [
lazy-regex = "^3.1" "apple-native",
lazy_static = "^1.4" "windows-native",
"sync-secret-service",
] }
lazy-regex = "^3"
lazy_static = "^1"
log = "^0.4" log = "^0.4"
magic-crypt = "^3.1" magic-crypt = "^3"
notify = "=4.0.17" notify = "6"
notify-rust = { version = "^4.5", default-features = false, features = ["d"] } notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
nucleo = "0.5"
open = "^5.0" open = "^5.0"
rand = "^0.8.5" rand = "^0.8.5"
regex = "^1" regex = "^1"
remotefs = "^0.2.0" remotefs = "^0.3"
remotefs-aws-s3 = { version = "^0.2.4", default-features = false, features = [ remotefs-aws-s3 = { version = "^0.3", default-features = false, features = [
"find", "find",
"rustls", "rustls",
] } ] }
remotefs-kube = "0.2" remotefs-kube = "0.4"
remotefs-webdav = "^0.1.1" remotefs-webdav = "^0.2"
rpassword = "^7.0" rpassword = "^7"
self_update = { version = "^0.41", default-features = false, features = [ self_update = { version = "^0.41", default-features = false, features = [
"rustls", "rustls",
"archive-tar", "archive-tar",
@@ -74,19 +73,19 @@ self_update = { version = "^0.41", default-features = false, features = [
serde = { version = "^1", features = ["derive"] } serde = { version = "^1", features = ["derive"] }
simplelog = "^0.12" simplelog = "^0.12"
ssh2-config = "^0.2" ssh2-config = "^0.2"
tempfile = "^3.4" tempfile = "^3"
thiserror = "^1" thiserror = "^1"
tokio = { version = "=1.38.1", features = ["rt"] } tokio = { version = "=1.38.1", features = ["rt"] }
toml = "^0.8" toml = "^0.8"
tui-realm-stdlib = "^1.3.1" tui-realm-stdlib = "^1.3"
tuirealm = "^1.9.1" tuirealm = "^1.9"
unicode-width = "^0.1" unicode-width = "^0.2"
version-compare = "^0.2" version-compare = "^0.2"
whoami = "^1.4" whoami = "^1.5"
wildmatch = "^2.1" wildmatch = "^2"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "^1.3" pretty_assertions = "^1"
serial_test = "^3" serial_test = "^3"
[build-dependencies] [build-dependencies]
@@ -95,21 +94,22 @@ cfg_aliases = "0.2"
[features] [features]
default = ["smb", "with-keyring"] default = ["smb", "with-keyring"]
github-actions = [] github-actions = []
with-keyring = ["keyring"] isolated-tests = []
smb = ["remotefs-smb"] smb = ["remotefs-smb"]
with-keyring = ["keyring"]
[target."cfg(not(target_os = \"macos\"))".dependencies] [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\")"]
[target."cfg(target_family = \"windows\")".dependencies] [target."cfg(target_family = \"windows\")".dependencies]
remotefs-ftp = { version = "^0.1.2", features = ["native-tls"] } remotefs-ftp = { version = "^0.2", features = ["native-tls"] }
remotefs-ssh = "^0.3.1" remotefs-ssh = "^0.4"
[target."cfg(target_family = \"unix\")"] [target."cfg(target_family = \"unix\")"]
[target."cfg(target_family = \"unix\")".dependencies] [target."cfg(target_family = \"unix\")".dependencies]
remotefs-ftp = { version = "^0.1.2", features = ["vendored", "native-tls"] } remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
remotefs-ssh = { version = "^0.3.1", features = ["ssh2-vendored"] } remotefs-ssh = { version = "^0.4", features = ["ssh2-vendored"] }
users = "0.11.0" users = "0.11.0"
[profile.dev] [profile.dev]
+1 -6
View File
@@ -71,7 +71,7 @@
</p> </p>
<p align="center">Developed by <a href="https://veeso.dev/" target="_blank">@veeso</a></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"> <p align="center">
<a href="https://opensource.org/licenses/MIT" <a href="https://opensource.org/licenses/MIT"
@@ -116,11 +116,6 @@
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg" src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
alt="Windows CI" alt="Windows CI"
/></a> /></a>
<a href="https://coveralls.io/github/veeso/termscp"
><img
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
alt="Coveralls"
/></a>
</p> </p>
--- ---
+1 -1
View File
@@ -71,7 +71,7 @@
</p> </p>
<p align="center">Entwickelt von <a href="https://veeso.dev/" target="_blank">@veeso</a></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"> <p align="center">
<a href="https://opensource.org/licenses/MIT" <a href="https://opensource.org/licenses/MIT"
+1 -1
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 Falls Sie eine Verbindung zu Kube herstellen möchten, verwenden Sie die folgende Syntax
```txt ```txt
kube://<container>@<pod></path> kube://[namespace][@<cluster_url>][$</path>]
``` ```
#### SMB Adressargument #### SMB Adressargument
+1 -1
View File
@@ -71,7 +71,7 @@
</p> </p>
<p align="center">Desarrollado por <a href="https://veeso.dev/" target="_blank">@veeso</a></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"> <p align="center">
<a href="https://opensource.org/licenses/MIT" <a href="https://opensource.org/licenses/MIT"
+1 -1
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 En caso de que quieras conectarte a Kube, utiliza la siguiente sintaxis
```txt ```txt
kube://<container>@<pod></path> kube://[namespace][@<cluster_url>][$</path>]
``` ```
#### Argumento de dirección de WebDAV #### Argumento de dirección de WebDAV
+1 -1
View File
@@ -71,7 +71,7 @@
</p> </p>
<p align="center">Développé par <a href="https://veeso.dev/" target="_blank">@veeso</a></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"> <p align="center">
<a href="https://opensource.org/licenses/MIT" <a href="https://opensource.org/licenses/MIT"
+1 -1
View File
@@ -108,7 +108,7 @@ s3://buckethead@eu-central-1:default:/assets
Si vous souhaitez vous connecter à Kube, utilisez la syntaxe suivante Si vous souhaitez vous connecter à Kube, utilisez la syntaxe suivante
```txt ```txt
kube://<container>@<pod></path> kube://[namespace][@<cluster_url>][$</path>]
``` ```
#### Argument d'adresse WebDAV #### Argument d'adresse WebDAV
+1 -1
View File
@@ -71,7 +71,7 @@
</p> </p>
<p align="center">Sviluppato da <a href="https://veeso.dev/" target="_blank">@veeso</a></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"> <p align="center">
<a href="https://opensource.org/licenses/MIT" <a href="https://opensource.org/licenses/MIT"
+1 -1
View File
@@ -106,7 +106,7 @@ s3://buckethead@eu-central-1:default:/assets
Nel caso tu voglia connetterti a Kube usa la seguente sintassi Nel caso tu voglia connetterti a Kube usa la seguente sintassi
```txt ```txt
kube://<container>@<pod></path> kube://[namespace][@<cluster_url>][$</path>]
``` ```
#### Argomento indirizzo per WebDAV #### Argomento indirizzo per WebDAV
+1 -1
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 In case you want to connect to Kube use the following syntax
```txt ```txt
kube://<container>@<pod></path> kube://[namespace][@<cluster_url>][$</path>]
``` ```
#### WebDAV address argument #### WebDAV address argument
+1 -1
View File
@@ -71,7 +71,7 @@
</p> </p>
<p align="center">Desenvolvido por <a href="https://veeso.dev/" target="_blank">@veeso</a></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"> <p align="center">
<a href="https://opensource.org/licenses/MIT" <a href="https://opensource.org/licenses/MIT"
+1 -1
View File
@@ -111,7 +111,7 @@ s3://buckethead@eu-central-1:default:/assets
Caso queira se conectar ao Kube, use a seguinte sintaxe Caso queira se conectar ao Kube, use a seguinte sintaxe
```txt ```txt
kube://<container>@<pod></path> kube://[namespace][@<cluster_url>][$</path>]
``` ```
#### Argumento de Endereço do WebDAV #### Argumento de Endereço do WebDAV
+1 -1
View File
@@ -71,7 +71,7 @@
</p> </p>
<p align="center">由 <a href="https://veeso.dev/" target="_blank">@veeso</a> 开发</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"> <p align="center">
<a href="https://opensource.org/licenses/MIT" <a href="https://opensource.org/licenses/MIT"
+1 -1
View File
@@ -108,7 +108,7 @@ s3://buckethead@eu-central-1:default:/assets
如果您想连接到 Kube,请使用以下语法 如果您想连接到 Kube,请使用以下语法
```txt ```txt
kube://<container>@<pod></path> kube://[namespace][@<cluster_url>][$</path>]
``` ```
#### WebDAV 地址参数 #### WebDAV 地址参数
+1 -1
View File
@@ -8,7 +8,7 @@
# -f, -y, --force, --yes # -f, -y, --force, --yes
# Skip the confirmation prompt during installation # 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}" GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb" DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb" DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
+2 -2
View File
@@ -35,7 +35,7 @@
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks <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, since last release, so if the latest version is not available yet,
you can install it downloading the ZIP file from</span> 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> target="_blank">Github</a>
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span> <span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
</p> </p>
@@ -74,7 +74,7 @@
On Debian based distros, you can install termscp using the Deb On Debian based distros, you can install termscp using the Deb
package via: package via:
</p> </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> sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
</div> </div>
<h3> <h3>
+1 -1
View File
@@ -12,7 +12,7 @@
</button> </button>
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50"> <div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
<p class="text-lg"> <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> <a href="/get-started.html" translate="intro.here">here!</a>
</p> </p>
</div> </div>
+1 -1
View File
@@ -12,7 +12,7 @@
"intro": { "intro": {
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV", "caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "Get started →", "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", "here": "here",
"features": { "features": {
"handy": { "handy": {
+1 -1
View File
@@ -12,7 +12,7 @@
"intro": { "intro": {
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV", "caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "Para iniciar →", "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ì", "here": "aquì",
"features": { "features": {
"handy": { "handy": {
+1 -1
View File
@@ -12,7 +12,7 @@
"intro": { "intro": {
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV", "caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "Pour commencer →", "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", "here": "ici",
"features": { "features": {
"handy": { "handy": {
+1 -1
View File
@@ -12,7 +12,7 @@
"intro": { "intro": {
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3", "caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
"getStarted": "Installa termscp →", "getStarted": "Installa termscp →",
"versionAlert": "termscp 0.14.0 è ORA disponbile! Scaricalo da", "versionAlert": "termscp 0.15.0 è ORA disponbile! Scaricalo da",
"here": "qui", "here": "qui",
"features": { "features": {
"handy": { "handy": {
+1 -1
View File
@@ -12,7 +12,7 @@
"intro": { "intro": {
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV", "caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "开始 →", "getStarted": "开始 →",
"versionAlert": "termscp 0.14.0 现已发布! 从下载", "versionAlert": "termscp 0.15.0 现已发布! 从下载",
"here": "这里", "here": "这里",
"features": { "features": {
"handy": { "handy": {
-7
View File
@@ -351,8 +351,6 @@ mod tests {
#[test] #[test]
fn bookmark_from_kube_ftparams() { fn bookmark_from_kube_ftparams() {
let params = ProtocolParams::Kube(KubeProtocolParams { let params = ProtocolParams::Kube(KubeProtocolParams {
pod: "pod".to_string(),
container: "container".to_string(),
namespace: Some("default".to_string()), namespace: Some("default".to_string()),
username: Some("root".to_string()), username: Some("root".to_string()),
cluster_url: Some("https://localhost:6443".to_string()), cluster_url: Some("https://localhost:6443".to_string()),
@@ -368,8 +366,6 @@ mod tests {
assert!(bookmark.username.is_none()); assert!(bookmark.username.is_none());
assert!(bookmark.password.is_none()); assert!(bookmark.password.is_none());
let kube: &KubeParams = bookmark.kube.as_ref().unwrap(); 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.namespace.as_deref().unwrap(), "default");
assert_eq!( assert_eq!(
kube.cluster_url.as_deref().unwrap(), kube.cluster_url.as_deref().unwrap(),
@@ -494,8 +490,6 @@ mod tests {
remote_path: Some(PathBuf::from("/tmp")), remote_path: Some(PathBuf::from("/tmp")),
local_path: Some(PathBuf::from("/usr")), local_path: Some(PathBuf::from("/usr")),
kube: Some(KubeParams { kube: Some(KubeParams {
pod_name: String::from("pod"),
container: String::from("container"),
namespace: Some(String::from("default")), namespace: Some(String::from("default")),
cluster_url: Some(String::from("https://localhost:6443")), cluster_url: Some(String::from("https://localhost:6443")),
username: Some(String::from("root")), username: Some(String::from("root")),
@@ -516,7 +510,6 @@ mod tests {
std::path::Path::new("/usr") std::path::Path::new("/usr")
); );
let gparams = params.params.kube_params().unwrap(); 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.namespace.as_deref().unwrap(), "default");
assert_eq!( assert_eq!(
gparams.cluster_url.as_deref().unwrap(), gparams.cluster_url.as_deref().unwrap(),
-6
View File
@@ -5,8 +5,6 @@ use crate::filetransfer::params::KubeProtocolParams;
/// Extra Connection parameters for Kube protocol /// Extra Connection parameters for Kube protocol
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)] #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
pub struct KubeParams { pub struct KubeParams {
pub pod_name: String,
pub container: String,
pub namespace: Option<String>, pub namespace: Option<String>,
pub cluster_url: Option<String>, pub cluster_url: Option<String>,
pub username: Option<String>, pub username: Option<String>,
@@ -17,8 +15,6 @@ pub struct KubeParams {
impl From<KubeParams> for KubeProtocolParams { impl From<KubeParams> for KubeProtocolParams {
fn from(value: KubeParams) -> Self { fn from(value: KubeParams) -> Self {
Self { Self {
pod: value.pod_name,
container: value.container,
namespace: value.namespace, namespace: value.namespace,
cluster_url: value.cluster_url, cluster_url: value.cluster_url,
username: value.username, username: value.username,
@@ -31,8 +27,6 @@ impl From<KubeParams> for KubeProtocolParams {
impl From<KubeProtocolParams> for KubeParams { impl From<KubeProtocolParams> for KubeParams {
fn from(value: KubeProtocolParams) -> Self { fn from(value: KubeProtocolParams) -> Self {
Self { Self {
pod_name: value.pod,
container: value.container,
namespace: value.namespace, namespace: value.namespace,
cluster_url: value.cluster_url, cluster_url: value.cluster_url,
username: value.username, username: value.username,
+39 -6
View File
@@ -412,8 +412,6 @@ mod tests {
assert_eq!(host.password, None); assert_eq!(host.password, None);
assert_eq!(host.protocol, FileTransferProtocol::Kube); assert_eq!(host.protocol, FileTransferProtocol::Kube);
let kube = host.kube.as_ref().unwrap(); 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.namespace.as_deref().unwrap(), "my-namespace");
assert_eq!(kube.cluster_url.as_deref().unwrap(), "https://my-cluster"); assert_eq!(kube.cluster_url.as_deref().unwrap(), "https://my-cluster");
assert_eq!(kube.username.as_deref().unwrap(), "my-username"); assert_eq!(kube.username.as_deref().unwrap(), "my-username");
@@ -515,8 +513,6 @@ mod tests {
s3: None, s3: None,
smb: None, smb: None,
kube: Some(KubeParams { kube: Some(KubeParams {
pod_name: "my-pod".to_string(),
container: "my-container".to_string(),
namespace: Some("my-namespace".to_string()), namespace: Some("my-namespace".to_string()),
cluster_url: Some("https://my-cluster".to_string()), cluster_url: Some("https://my-cluster".to_string()),
username: Some("my-username".to_string()), username: Some("my-username".to_string()),
@@ -593,6 +589,22 @@ mod tests {
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err()); 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 { fn create_good_toml_bookmarks() -> tempfile::NamedTempFile {
// Write // Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap(); let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
@@ -617,8 +629,6 @@ mod tests {
[bookmarks.pod] [bookmarks.pod]
protocol = "KUBE" protocol = "KUBE"
[bookmarks.pod.kube] [bookmarks.pod.kube]
pod_name = "my-pod"
container = "my-container"
namespace = "my-namespace" namespace = "my-namespace"
cluster_url = "https://my-cluster" cluster_url = "https://my-cluster"
username = "my-username" username = "my-username"
@@ -644,6 +654,29 @@ mod tests {
tmpfile 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 { fn create_bad_toml_bookmarks() -> tempfile::NamedTempFile {
// Write // Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap(); let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
+3
View File
@@ -30,6 +30,7 @@ pub enum FileSorting {
ModifyTime, ModifyTime,
CreationTime, CreationTime,
Size, Size,
None,
} }
/// GroupDirs defines how directories should be grouped in sorting files /// 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::CreationTime => self.sort_files_by_creation_time(),
FileSorting::ModifyTime => self.sort_files_by_mtime(), FileSorting::ModifyTime => self.sort_files_by_mtime(),
FileSorting::Size => self.sort_files_by_size(), FileSorting::Size => self.sort_files_by_size(),
FileSorting::None => {}
} }
// Directories first (NOTE: MUST COME AFTER OTHER SORTING) // Directories first (NOTE: MUST COME AFTER OTHER SORTING)
// Group directories if necessary // Group directories if necessary
@@ -245,6 +247,7 @@ impl std::fmt::Display for FileSorting {
FileSorting::ModifyTime => "by_mtime", FileSorting::ModifyTime => "by_mtime",
FileSorting::Name => "by_name", FileSorting::Name => "by_name",
FileSorting::Size => "by_size", FileSorting::Size => "by_size",
FileSorting::None => "none",
} }
) )
} }
+2 -4
View File
@@ -8,7 +8,7 @@ use std::sync::Arc;
use remotefs::RemoteFs; use remotefs::RemoteFs;
use remotefs_aws_s3::AwsS3Fs; use remotefs_aws_s3::AwsS3Fs;
use remotefs_ftp::FtpFs; use remotefs_ftp::FtpFs;
use remotefs_kube::KubeFs; use remotefs_kube::KubeMultiPodFs as KubeFs;
#[cfg(smb_unix)] #[cfg(smb_unix)]
use remotefs_smb::SmbOptions; use remotefs_smb::SmbOptions;
#[cfg(smb)] #[cfg(smb)]
@@ -119,7 +119,7 @@ impl Builder {
.build() .build()
.expect("Unable to create tokio runtime"), .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() { if let Some(config) = params.config() {
kube_fs.config(config) kube_fs.config(config)
} else { } else {
@@ -281,8 +281,6 @@ mod test {
#[test] #[test]
fn test_should_build_kube_fs() { fn test_should_build_kube_fs() {
let params = ProtocolParams::Kube(KubeProtocolParams { let params = ProtocolParams::Kube(KubeProtocolParams {
pod: "pod".to_string(),
container: "container".to_string(),
namespace: Some("namespace".to_string()), namespace: Some("namespace".to_string()),
cluster_url: Some("cluster_url".to_string()), cluster_url: Some("cluster_url".to_string()),
username: Some("username".to_string()), username: Some("username".to_string()),
-2
View File
@@ -3,8 +3,6 @@ use remotefs_kube::Config;
/// Protocol params used by WebDAV /// Protocol params used by WebDAV
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct KubeProtocolParams { pub struct KubeProtocolParams {
pub pod: String,
pub container: String,
pub namespace: Option<String>, pub namespace: Option<String>,
pub cluster_url: Option<String>, pub cluster_url: Option<String>,
pub username: Option<String>, pub username: Option<String>,
+20 -114
View File
@@ -16,7 +16,6 @@ use filetime::{self, FileTime};
use remotefs::fs::UnixPex; use remotefs::fs::UnixPex;
use remotefs::fs::{File, FileType, Metadata}; use remotefs::fs::{File, FileType, Metadata};
use thiserror::Error; use thiserror::Error;
use wildmatch::WildMatch;
// Locals // Locals
use crate::utils::path; use crate::utils::path;
@@ -112,7 +111,7 @@ impl Localhost {
)); ));
} }
// Retrieve files for provided path // 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, Ok(files) => files,
Err(err) => { Err(err) => {
error!( error!(
@@ -131,12 +130,6 @@ impl Localhost {
self.wrkdir.clone() 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 /// Change working directory with the new provided directory
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> { pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
let new_dir: PathBuf = self.to_path(new_dir); let new_dir: PathBuf = self.to_path(new_dir);
@@ -164,7 +157,7 @@ impl Localhost {
// Change dir // Change dir
self.wrkdir = new_dir; self.wrkdir = new_dir;
// Scan new directory // 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, Ok(files) => files,
Err(err) => { Err(err) => {
error!("Could not scan new directory: {}", err); error!("Could not scan new directory: {}", err);
@@ -204,7 +197,7 @@ impl Localhost {
Ok(_) => { Ok(_) => {
// Update dir // Update dir
if dir_name.is_relative() { 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()); info!("Created directory {}", dir_path.display());
Ok(()) Ok(())
@@ -237,7 +230,7 @@ impl Localhost {
match std::fs::remove_dir_all(entry.path()) { match std::fs::remove_dir_all(entry.path()) {
Ok(_) => { Ok(_) => {
// Update dir // 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()); info!("Removed directory {}", entry.path().display());
Ok(()) Ok(())
} }
@@ -265,7 +258,7 @@ impl Localhost {
match std::fs::remove_file(entry.path()) { match std::fs::remove_file(entry.path()) {
Ok(_) => { Ok(_) => {
// Update dir // 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()); info!("Removed file {}", entry.path().display());
Ok(()) Ok(())
} }
@@ -286,7 +279,7 @@ impl Localhost {
match std::fs::rename(entry.path(), dst_path) { match std::fs::rename(entry.path(), dst_path) {
Ok(_) => { Ok(_) => {
// Scan dir // Scan dir
self.files = self.scan_dir(self.wrkdir.as_path())?; self.files = self.list_dir(self.wrkdir.as_path())?;
debug!( debug!(
"Moved file {} to {}", "Moved file {} to {}",
entry.path().display(), entry.path().display(),
@@ -327,7 +320,7 @@ impl Localhost {
self.mkdir(dst.as_path())?; self.mkdir(dst.as_path())?;
} }
// Scan dir // Scan dir
let dir_files: Vec<File> = self.scan_dir(entry.path())?; let dir_files: Vec<File> = self.list_dir(entry.path())?;
// Iterate files // Iterate files
for dir_entry in dir_files.iter() { for dir_entry in dir_files.iter() {
// Calculate dst // Calculate dst
@@ -362,11 +355,11 @@ impl Localhost {
match dst.is_dir() { match dst.is_dir() {
true => { true => {
if dst == self.pwd().as_path() { 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() { } else if let Some(parent) = dst.parent() {
// If parent is pwd, scan directory // If parent is pwd, scan directory
if parent == self.pwd().as_path() { 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 let Some(parent) = dst.parent() {
// If parent is pwd, scan directory // If parent is pwd, scan directory
if parent == self.pwd().as_path() { 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 /// 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()); info!("Reading directory {}", dir.display());
match std::fs::read_dir(dir) { match std::fs::read_dir(dir) {
Ok(e) => { 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 /// Create a symlink at path pointing at target
#[cfg(unix)] #[cfg(unix)]
pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> { 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 /// Convert path to absolute path
fn to_path(&self, p: &Path) -> PathBuf { fn to_path(&self, p: &Path) -> PathBuf {
path::absolutize(self.wrkdir.as_path(), p) path::absolutize(self.wrkdir.as_path(), p)
@@ -658,7 +610,7 @@ mod tests {
use super::*; use super::*;
#[cfg(unix)] #[cfg(unix)]
use crate::utils::test_helpers::make_fsentry; 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] #[test]
fn test_host_error_new() { fn test_host_error_new() {
@@ -711,19 +663,6 @@ mod tests {
assert_eq!(host.pwd(), PathBuf::from("/dev")); 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] #[test]
#[cfg(unix)] #[cfg(unix)]
fn test_host_localhost_change_dir() { fn test_host_localhost_change_dir() {
@@ -813,7 +752,7 @@ mod tests {
.is_ok()); .is_ok());
// Get dir // Get dir
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); 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 // Verify files
let file_0: &File = files.get(0).unwrap(); let file_0: &File = files.get(0).unwrap();
if file_0.name() == *"foo.txt" { if file_0.name() == *"foo.txt" {
@@ -841,10 +780,10 @@ mod tests {
fn test_host_localhost_mkdir() { fn test_host_localhost_mkdir() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().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_eq!(files.len(), 0); // There should be 0 files now
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); 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 assert_eq!(files.len(), 1); // There should be 1 file now
// Try to re-create directory // Try to re-create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err()); assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
@@ -868,17 +807,17 @@ mod tests {
// Create sample file // Create sample file
assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); 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 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.len(), 1); // There should be 1 file now
// Remove file // Remove file
assert!(host.remove(files.get(0).unwrap()).is_ok()); assert!(host.remove(files.get(0).unwrap()).is_ok());
// There should be 0 files now // 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 assert_eq!(files.len(), 0); // There should be 0 files now
// Create directory // Create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
// Delete directory // 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_eq!(files.len(), 1); // There should be 1 file now
assert!(host.remove(files.get(0).unwrap()).is_ok()); assert!(host.remove(files.get(0).unwrap()).is_ok());
// Remove unexisting directory // Remove unexisting directory
@@ -899,7 +838,7 @@ mod tests {
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str()); PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str());
assert!(StdFile::create(src_path.as_path()).is_ok()); assert!(StdFile::create(src_path.as_path()).is_ok());
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().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(), 1); // There should be 1 file now assert_eq!(files.len(), 1); // There should be 1 file now
assert_eq!(files.get(0).unwrap().name(), "foo.txt"); assert_eq!(files.get(0).unwrap().name(), "foo.txt");
// Rename file // Rename file
@@ -909,7 +848,7 @@ mod tests {
.rename(files.get(0).unwrap(), dst_path.as_path()) .rename(files.get(0).unwrap(), dst_path.as_path())
.is_ok()); .is_ok());
// There should be still 1 file now, but named bar.txt // 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.len(), 1); // There should be 0 files now
assert_eq!(files.get(0).unwrap().name(), "bar.txt"); assert_eq!(files.get(0).unwrap().name(), "bar.txt");
// Fail // Fail
@@ -1084,39 +1023,6 @@ mod tests {
assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5")); 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)] #[cfg(unix)]
#[test] #[test]
fn should_create_symlink() { fn should_create_symlink() {
+14 -8
View File
@@ -145,10 +145,13 @@ mod test {
} }
#[test] #[test]
#[cfg(not(all( #[cfg(all(
any(target_os = "macos", target_os = "freebsd"), not(all(
feature = "github-actions" any(target_os = "macos", target_os = "freebsd"),
)))] feature = "github-actions"
)),
not(feature = "isolated-tests")
))]
fn auto_update() { fn auto_update() {
// Wno version // Wno version
assert_eq!( assert_eq!(
@@ -162,10 +165,13 @@ mod test {
} }
#[test] #[test]
#[cfg(not(all( #[cfg(all(
any(target_os = "macos", target_os = "freebsd"), not(all(
feature = "github-actions" any(target_os = "macos", target_os = "freebsd"),
)))] feature = "github-actions"
)),
not(feature = "isolated-tests")
))]
fn check_for_updates() { fn check_for_updates() {
println!("{:?}", Update::is_new_version_available()); println!("{:?}", Update::is_new_version_available());
assert!(Update::is_new_version_available().is_ok()); assert!(Update::is_new_version_available().is_ok());
+4 -3
View File
@@ -82,6 +82,7 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
#[cfg(not(feature = "isolated-tests"))]
fn test_system_keys_keyringstorage() { fn test_system_keys_keyringstorage() {
let username: String = username(); let username: String = username();
let storage: KeyringStorage = KeyringStorage::new(username.as_str()); let storage: KeyringStorage = KeyringStorage::new(username.as_str());
@@ -89,17 +90,17 @@ mod tests {
let app_name: &str = "termscp-test2"; let app_name: &str = "termscp-test2";
let secret: &str = "Th15-15/My-Супер-Секрет"; let secret: &str = "Th15-15/My-Супер-Секрет";
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap(); let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
let _ = kring.delete_password(); let _ = kring.delete_credential();
drop(kring); drop(kring);
// Secret should not exist // Secret should not exist
assert!(storage.get_key(app_name).is_err()); assert!(storage.get_key(app_name).is_err());
// Write secret // Write secret
assert!(storage.set_key(app_name, secret).is_ok()); assert!(storage.set_key(app_name, secret).is_ok());
// Get secret // 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... // Delete the key manually...
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap(); let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
assert!(kring.delete_password().is_ok()); assert!(kring.delete_credential().is_ok());
} }
} }
+75 -19
View File
@@ -12,7 +12,7 @@ use std::time::Duration;
pub use change::FsChange; pub use change::FsChange;
use notify::{ use notify::{
watcher, DebouncedEvent, Error as WatcherError, RecommendedWatcher, RecursiveMode, Watcher, Config, Error as WatcherError, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
}; };
use thiserror::Error; use thiserror::Error;
@@ -27,6 +27,8 @@ pub enum FsWatcherError {
PathNotWatched, PathNotWatched,
#[error("unable to watch path, since it's already watched")] #[error("unable to watch path, since it's already watched")]
PathAlreadyWatched, PathAlreadyWatched,
#[error("unknown event: {0}")]
UnknownEvent(&'static str),
#[error("worker error: {0}")] #[error("worker error: {0}")]
WorkerError(WatcherError), 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 /// File system watcher
pub struct FsWatcher { pub struct FsWatcher {
paths: HashMap<PathBuf, PathBuf>, paths: HashMap<PathBuf, PathBuf>,
receiver: Receiver<DebouncedEvent>, receiver: Receiver<notify::Result<Event>>,
watcher: RecommendedWatcher, watcher: RecommendedWatcher,
} }
@@ -52,29 +105,32 @@ impl FsWatcher {
Ok(Self { Ok(Self {
paths: HashMap::default(), paths: HashMap::default(),
receiver, receiver,
watcher: watcher(tx, delay)?, watcher: RecommendedWatcher::new(tx, Config::default().with_poll_interval(delay))?,
}) })
} }
/// Poll searching for the first available disk change /// Poll searching for the first available disk change
pub fn poll(&self) -> FsWatcherResult<Option<FsChange>> { pub fn poll(&self) -> FsWatcherResult<Option<FsChange>> {
match self.receiver.recv_timeout(Duration::from_millis(1)) { let res = match self.receiver.recv_timeout(Duration::from_millis(1)) {
Ok(DebouncedEvent::Rename(source, dest)) => Ok(self.build_fs_move(source, dest)), Ok(res) => res,
Ok(DebouncedEvent::Remove(p)) => Ok(self.build_fs_remove(p)), Err(RecvTimeoutError::Timeout) => return Ok(None),
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),
Err(RecvTimeoutError::Disconnected) => panic!("File watcher died"), 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)
}
} }
} }
-2
View File
@@ -193,8 +193,6 @@ impl AuthActivity {
} }
fn load_bookmark_kube_into_gui(&mut self, params: KubeProtocolParams) { 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_cluster_url(params.cluster_url.as_deref().unwrap_or(""));
self.mount_kube_namespace(params.namespace.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("")); self.mount_kube_client_cert(params.client_cert.as_deref().unwrap_or(""));
-68
View File
@@ -832,40 +832,6 @@ impl Component<Msg, NoUserEvent> for InputWebDAVUri {
// kube // 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)] #[derive(MockComponent)]
pub struct InputKubeNamespace { pub struct InputKubeNamespace {
component: Input, 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)] #[derive(MockComponent)]
pub struct InputKubeUsername { pub struct InputKubeUsername {
component: Input, component: Input,
+5 -6
View File
@@ -16,12 +16,11 @@ pub use bookmarks::{
#[cfg(unix)] #[cfg(unix)]
pub use form::InputSmbWorkgroup; pub use form::InputSmbWorkgroup;
pub use form::{ pub use form::{
InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeContainer, InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeNamespace,
InputKubeNamespace, InputKubePodName, InputKubeUsername, InputLocalDirectory, InputPassword, InputKubeUsername, InputLocalDirectory, InputPassword, InputPort, InputRemoteDirectory,
InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile, InputS3Region,
InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputSmbShare,
InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, ProtocolRadio, InputUsername, InputWebDAVUri, ProtocolRadio, RadioS3NewPathStyle,
RadioS3NewPathStyle,
}; };
pub use popup::{ pub use popup::{
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup, ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
+1 -3
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 /// 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> { pub(super) fn collect_kube_host_params(&self) -> Result<FileTransferParams, &'static str> {
let params = self.get_kube_params_input(); let params = self.get_kube_params_input();
if params.pod.is_empty() {
return Err("Invalid pod name");
}
Ok(FileTransferParams { Ok(FileTransferParams {
protocol: FileTransferProtocol::Kube, protocol: FileTransferProtocol::Kube,
params: ProtocolParams::Kube(params), params: ProtocolParams::Kube(params),
-6
View File
@@ -48,8 +48,6 @@ pub enum Id {
InfoPopup, InfoPopup,
InstallUpdatePopup, InstallUpdatePopup,
Keybindings, Keybindings,
KubePodName,
KubeContainer,
KubeNamespace, KubeNamespace,
KubeClusterUrl, KubeClusterUrl,
KubeUsername, KubeUsername,
@@ -119,10 +117,6 @@ pub enum UiMsg {
CloseKeybindingsPopup, CloseKeybindingsPopup,
CloseQuitPopup, CloseQuitPopup,
CloseSaveBookmark, CloseSaveBookmark,
KubePodNameBlurDown,
KubePodNameBlurUp,
KubeContainerBlurDown,
KubeContainerBlurUp,
KubeNamespaceBlurDown, KubeNamespaceBlurDown,
KubeNamespaceBlurUp, KubeNamespaceBlurUp,
KubeClusterUrlBlurDown, KubeClusterUrlBlurDown,
+4 -16
View File
@@ -70,7 +70,7 @@ impl AuthActivity {
InputMask::Generic => &Id::Password, InputMask::Generic => &Id::Password,
InputMask::Smb => &Id::Password, InputMask::Smb => &Id::Password,
InputMask::AwsS3 => &Id::S3Bucket, InputMask::AwsS3 => &Id::S3Bucket,
InputMask::Kube => &Id::KubePodName, InputMask::Kube => &Id::KubeNamespace,
InputMask::WebDAV => &Id::Password, InputMask::WebDAV => &Id::Password,
}) })
.is_ok()); .is_ok());
@@ -84,7 +84,7 @@ impl AuthActivity {
InputMask::Generic => &Id::Password, InputMask::Generic => &Id::Password,
InputMask::Smb => &Id::Password, InputMask::Smb => &Id::Password,
InputMask::AwsS3 => &Id::S3Bucket, InputMask::AwsS3 => &Id::S3Bucket,
InputMask::Kube => &Id::KubePodName, InputMask::Kube => &Id::KubeNamespace,
InputMask::WebDAV => &Id::Password, InputMask::WebDAV => &Id::Password,
}) })
.is_ok()); .is_ok());
@@ -210,7 +210,7 @@ impl AuthActivity {
InputMask::Generic => &Id::Address, InputMask::Generic => &Id::Address,
InputMask::Smb => &Id::Address, InputMask::Smb => &Id::Address,
InputMask::AwsS3 => &Id::S3Bucket, InputMask::AwsS3 => &Id::S3Bucket,
InputMask::Kube => &Id::KubePodName, InputMask::Kube => &Id::KubeNamespace,
InputMask::WebDAV => &Id::WebDAVUri, InputMask::WebDAV => &Id::WebDAVUri,
}) })
.is_ok()); .is_ok());
@@ -305,23 +305,11 @@ impl AuthActivity {
UiMsg::KubeClientKeyBlurUp => { UiMsg::KubeClientKeyBlurUp => {
assert!(self.app.active(&Id::KubeClientCert).is_ok()); 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 => { UiMsg::KubeNamespaceBlurDown => {
assert!(self.app.active(&Id::KubeClusterUrl).is_ok()); assert!(self.app.active(&Id::KubeClusterUrl).is_ok());
} }
UiMsg::KubeNamespaceBlurUp => { UiMsg::KubeNamespaceBlurUp => {
assert!(self.app.active(&Id::KubeContainer).is_ok()); assert!(self.app.active(&Id::Protocol).is_ok());
} }
UiMsg::KubeClusterUrlBlurDown => { UiMsg::KubeClusterUrlBlurDown => {
assert!(self.app.active(&Id::KubeUsername).is_ok()); assert!(self.app.active(&Id::KubeUsername).is_ok());
+18 -147
View File
@@ -64,9 +64,7 @@ impl AuthActivity {
self.mount_kube_client_cert(""); self.mount_kube_client_cert("");
self.mount_kube_client_key(""); self.mount_kube_client_key("");
self.mount_kube_cluster_url(""); self.mount_kube_cluster_url("");
self.mount_kube_container("");
self.mount_kube_namespace(""); self.mount_kube_namespace("");
self.mount_kube_pod_name("");
self.mount_kube_username(""); self.mount_kube_username("");
self.mount_smb_share(""); self.mount_smb_share("");
#[cfg(unix)] #[cfg(unix)]
@@ -149,86 +147,18 @@ impl AuthActivity {
.direction(Direction::Vertical) .direction(Direction::Vertical)
.split(main_chunks[0]); .split(main_chunks[0]);
// Input mask chunks // Input mask chunks
let input_mask = match self.input_mask() { let input_mask = Layout::default()
InputMask::AwsS3 => Layout::default() .constraints(
.constraints( [
[ Constraint::Length(3), // uri
Constraint::Length(3), // bucket Constraint::Length(3), // username
Constraint::Length(3), // region Constraint::Length(3), // password
Constraint::Length(3), // profile Constraint::Length(3), // dir
Constraint::Length(3), // access_key ]
Constraint::Length(3), // remote directory .as_ref(),
] )
.as_ref(), .direction(Direction::Vertical)
) .split(auth_chunks[4]);
.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]),
};
// Create bookmark chunks // Create bookmark chunks
let bookmark_chunks = Layout::default() let bookmark_chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
@@ -347,7 +277,7 @@ impl AuthActivity {
.constraints( .constraints(
[ [
Constraint::Length(3), // Input form Constraint::Length(3), // Input form
Constraint::Length(2), // Yes/No Constraint::Length(4), // Yes/No
] ]
.as_ref(), .as_ref(),
) )
@@ -816,30 +746,6 @@ impl AuthActivity {
.is_ok()); .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) { pub(super) fn mount_kube_namespace(&mut self, value: &str) {
let color = self.theme().auth_port; let color = self.theme().auth_port;
assert!(self assert!(self
@@ -974,16 +880,12 @@ impl AuthActivity {
/// Collect s3 input values from view /// Collect s3 input values from view
pub(super) fn get_kube_params_input(&self) -> KubeProtocolParams { 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 namespace = self.get_input_kube_namespace();
let cluster_url = self.get_input_kube_cluster_url(); let cluster_url = self.get_input_kube_cluster_url();
let username = self.get_input_kube_username(); let username = self.get_input_kube_username();
let client_cert = self.get_input_kube_client_cert(); let client_cert = self.get_input_kube_client_cert();
let client_key = self.get_input_kube_client_key(); let client_key = self.get_input_kube_client_key();
KubeProtocolParams { KubeProtocolParams {
pod,
container,
namespace, namespace,
cluster_url, cluster_url,
username, username,
@@ -1069,10 +971,7 @@ impl AuthActivity {
pub(super) fn get_input_port(&self) -> u16 { pub(super) fn get_input_port(&self) -> u16 {
match self.app.state(&Id::Port) { match self.app.state(&Id::Port) {
Ok(State::One(StateValue::String(x))) => match u16::from_str(x.as_str()) { Ok(State::One(StateValue::String(x))) => u16::from_str(x.as_str()).unwrap_or_default(),
Ok(v) => v,
_ => 0,
},
_ => 0, _ => 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> { pub(super) fn get_input_kube_namespace(&self) -> Option<String> {
match self.app.state(&Id::KubeNamespace) { match self.app.state(&Id::KubeNamespace) {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x),
@@ -1285,15 +1170,13 @@ impl AuthActivity {
} }
ProtocolParams::Kube(params) => { ProtocolParams::Kube(params) => {
format!( format!(
"{}://{}@{}{}{}", "{}://{}{}",
protocol, protocol,
params.container,
params.pod,
params params
.namespace .namespace
.as_deref() .as_deref()
.map(|x| format!("/{x}")) .map(|x| format!("/{x}"))
.unwrap_or_default(), .unwrap_or_else(|| String::from("default")),
params params
.cluster_url .cluster_url
.as_deref() .as_deref()
@@ -1389,18 +1272,6 @@ impl AuthActivity {
/// Get the visible element in the kube form, based on current focus /// Get the visible element in the kube form, based on current focus
fn get_kube_view(&self) -> [Id; 4] { fn get_kube_view(&self) -> [Id; 4] {
match self.app.focus() { 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) => [ Some(&Id::KubeClientCert) => [
Id::KubeNamespace, Id::KubeNamespace,
Id::KubeClusterUrl, Id::KubeClusterUrl,
@@ -1426,10 +1297,10 @@ impl AuthActivity {
Id::LocalDirectory, Id::LocalDirectory,
], ],
_ => [ _ => [
Id::KubePodName,
Id::KubeContainer,
Id::KubeNamespace, Id::KubeNamespace,
Id::KubeClusterUrl, Id::KubeClusterUrl,
Id::KubeUsername,
Id::KubeClientCert,
], ],
} }
} }
+1 -14
View File
@@ -9,23 +9,10 @@ use super::super::browser::FileExplorerTab;
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload}; use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
impl FileTransferActivity { 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) { pub(crate) fn action_find_changedir(&mut self) {
// Match entry // Match entry
if let SelectedFile::One(entry) = self.get_found_selected_entries() { 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 // Get path: if a directory, use directory path; if it is a File, get parent path
let path = if entry.is_dir() { let path = if entry.is_dir() {
entry.path().to_path_buf() entry.path().to_path_buf()
@@ -27,8 +27,10 @@ pub(crate) mod open;
mod pending; mod pending;
pub(crate) mod rename; pub(crate) mod rename;
pub(crate) mod save; pub(crate) mod save;
pub(crate) mod scan;
pub(crate) mod submit; pub(crate) mod submit;
pub(crate) mod symlink; pub(crate) mod symlink;
pub(crate) mod walkdir;
pub(crate) mod watcher; pub(crate) mod watcher;
#[derive(Debug)] #[derive(Debug)]
@@ -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)),
}
}
}
@@ -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();
}
}
@@ -5,8 +5,7 @@
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table}; use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table};
use tuirealm::tui::layout::Corner; use tuirealm::tui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue}; use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue};
use super::{Msg, UiMsg}; use super::{Msg, UiMsg};
@@ -63,7 +62,7 @@ impl MockComponent for Log {
focus, focus,
None, None,
)) ))
.start_corner(Corner::BottomLeft) .direction(ListDirection::BottomToTop)
.highlight_symbol(">> ") .highlight_symbol(">> ")
.style(Style::default().bg(bg)) .style(Style::default().bg(bg))
.highlight_style(Style::default()); .highlight_style(Style::default());
@@ -17,12 +17,13 @@ mod transfer;
pub use misc::FooterBar; pub use misc::FooterBar;
pub use popups::{ pub use popups::{
ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup, 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, OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, 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; pub use self::log::Log;
@@ -2,6 +2,9 @@
//! //!
//! popups components //! popups components
mod chmod;
mod goto;
use std::time::UNIX_EPOCH; use std::time::UNIX_EPOCH;
use bytesize::ByteSize; use bytesize::ByteSize;
@@ -16,15 +19,13 @@ use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
#[cfg(unix)] #[cfg(unix)]
use users::{get_group_by_gid, get_user_by_uid}; 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::super::Browser;
use super::{Msg, PendingActionMsg, TransferMsg, UiMsg}; use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
use crate::explorer::FileSorting; use crate::explorer::FileSorting;
use crate::utils::fmt::fmt_time; use crate::utils::fmt::fmt_time;
mod chmod;
pub use chmod::ChmodPopup;
#[derive(MockComponent)] #[derive(MockComponent)]
pub struct CopyPopup { pub struct CopyPopup {
component: Input, component: Input,
@@ -131,7 +132,10 @@ impl FilterPopup {
"regex or wildmatch", "regex or wildmatch",
Style::default().fg(Color::Rgb(128, 128, 128)), 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)] #[derive(MockComponent)]
pub struct KeybindingsPopup { pub struct KeybindingsPopup {
component: List, component: List,
@@ -1675,6 +1509,7 @@ impl SortingPopup {
FileSorting::ModifyTime => 1, FileSorting::ModifyTime => 1,
FileSorting::Name => 0, FileSorting::Name => 0,
FileSorting::Size => 3, FileSorting::Size => 3,
FileSorting::None => 0,
}), }),
} }
} }
@@ -1778,6 +1613,7 @@ fn file_sorting_label(sorting: FileSorting) -> &'static str {
FileSorting::ModifyTime => "By modify time", FileSorting::ModifyTime => "By modify time",
FileSorting::Name => "By name", FileSorting::Name => "By name",
FileSorting::Size => "By size", 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)] #[derive(MockComponent)]
pub struct WatchedPathsList { pub struct WatchedPathsList {
component: List, component: List,
@@ -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);
}
}
@@ -6,9 +6,8 @@ use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::props::{ use tuirealm::props::{
Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers, Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers,
}; };
use tuirealm::tui::layout::Corner;
use tuirealm::tui::text::{Line, Span}; 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}; use tuirealm::{MockComponent, Props, State, StateValue};
pub const FILE_LIST_CMD_SELECT_ALL: &str = "A"; pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
@@ -235,7 +234,7 @@ impl MockComponent for FileList {
// Make list // Make list
let mut list = TuiList::new(list_items) let mut list = TuiList::new(list_items)
.block(div) .block(div)
.start_corner(Corner::TopLeft); .direction(ListDirection::TopToBottom);
if let Some(highlighted_color) = highlighted_color { if let Some(highlighted_color) = highlighted_color {
list = list.highlight_style( list = list.highlight_style(
Style::default() Style::default()
@@ -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),
}
}
}
@@ -2,14 +2,220 @@
//! //!
//! file transfer components //! file transfer components
use super::{Msg, TransferMsg, UiMsg};
mod file_list; mod file_list;
use file_list::FileList; mod file_list_with_search;
use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, Borders, Color, TextSpan}; 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)] #[derive(MockComponent)]
pub struct ExplorerFind { pub struct ExplorerFind {
@@ -261,7 +467,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Char('f'), code: Key::Char('f'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)), }) => Some(Msg::Transfer(TransferMsg::InitFuzzySearch)),
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Char('g'), code: Key::Char('g'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
@@ -457,7 +663,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Char('f'), code: Key::Char('f'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)), }) => Some(Msg::Transfer(TransferMsg::InitFuzzySearch)),
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Char('g'), code: Key::Char('g'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
+88 -12
View File
@@ -4,12 +4,15 @@
use std::path::Path; use std::path::Path;
use nucleo::Utf32String;
use remotefs::File; use remotefs::File;
use crate::explorer::builder::FileExplorerBuilder; use crate::explorer::builder::FileExplorerBuilder;
use crate::explorer::{FileExplorer, FileSorting, GroupDirs}; use crate::explorer::{FileExplorer, FileSorting};
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
const FUZZY_SEARCH_THRESHOLD: u16 = 50;
/// File explorer tab /// File explorer tab
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub enum FileExplorerTab { pub enum FileExplorerTab {
@@ -28,10 +31,10 @@ pub enum FoundExplorerTab {
/// Browser contains the browser options /// Browser contains the browser options
pub struct Browser { pub struct Browser {
local: FileExplorer, // Local File explorer state local: FileExplorer, // Local File explorer state
remote: FileExplorer, // Remote File explorer state remote: FileExplorer, // Remote File explorer state
found: Option<(FoundExplorerTab, FileExplorer)>, // File explorer for find result found: Option<Found>, // File explorer for find result
tab: FileExplorerTab, // Current selected tab tab: FileExplorerTab, // Current selected tab
pub sync_browsing: bool, 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 { pub fn local(&self) -> &FileExplorer {
&self.local &self.local
} }
@@ -64,17 +77,35 @@ impl Browser {
} }
pub fn found(&self) -> Option<&FileExplorer> { 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> { 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) { pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec<File>, wrkdir: &Path) {
let mut explorer = Self::build_found_explorer(wrkdir); let mut explorer = Self::build_found_explorer(wrkdir);
explorer.set_files(files); explorer.set_files(files.clone());
self.found = Some((tab, explorer)); self.found = Some(Found {
tab,
explorer,
search_results: files,
});
} }
pub fn del_found(&mut self) { pub fn del_found(&mut self) {
@@ -83,7 +114,7 @@ impl Browser {
/// Returns found tab if any /// Returns found tab if any
pub fn found_tab(&self) -> Option<FoundExplorerTab> { 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 { pub fn tab(&self) -> FileExplorerTab {
@@ -129,8 +160,8 @@ impl Browser {
/// Build explorer reading from `ConfigClient`, for found result (has some differences) /// Build explorer reading from `ConfigClient`, for found result (has some differences)
fn build_found_explorer(wrkdir: &Path) -> FileExplorer { fn build_found_explorer(wrkdir: &Path) -> FileExplorer {
FileExplorerBuilder::new() FileExplorerBuilder::new()
.with_file_sorting(FileSorting::Name) .with_file_sorting(FileSorting::None)
.with_group_dirs(Some(GroupDirs::First)) .with_group_dirs(None)
.with_hidden_files(true) .with_hidden_files(true)
.with_stack_size(0) .with_stack_size(0)
.with_formatter(Some( .with_formatter(Some(
@@ -139,3 +170,48 @@ impl Browser {
.build() .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(),
);
}
}
@@ -4,3 +4,4 @@
pub(crate) mod browser; pub(crate) mod browser;
pub(crate) mod transfer; pub(crate) mod transfer;
pub(crate) mod walkdir;
@@ -0,0 +1,4 @@
#[derive(Debug, Default)]
pub struct WalkdirStates {
pub aborted: bool,
}
+7 -7
View File
@@ -22,7 +22,7 @@ const LOG_CAPACITY: usize = 256;
impl FileTransferActivity { impl FileTransferActivity {
/// Call `Application::tick()` and process messages in `Update` /// Call `Application::tick()` and process messages in `Update`
pub(super) fn tick(&mut self) { pub(super) fn tick(&mut self) {
match self.app.tick(PollStrategy::UpTo(3)) { match self.app.tick(PollStrategy::UpTo(1)) {
Ok(messages) => { Ok(messages) => {
if !messages.is_empty() { if !messages.is_empty() {
self.redraw = true; self.redraw = true;
@@ -111,7 +111,9 @@ impl FileTransferActivity {
match &ft_params.params { match &ft_params.params {
ProtocolParams::Generic(params) => params.address.clone(), ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::AwsS3(params) => params.bucket_name.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::Smb(params) => params.address.clone(),
ProtocolParams::WebDAV(params) => params.uri.clone(), ProtocolParams::WebDAV(params) => params.uri.clone(),
} }
@@ -137,11 +139,9 @@ impl FileTransferActivity {
format!("Connecting to {}…", params.bucket_name) format!("Connecting to {}…", params.bucket_name)
} }
ProtocolParams::Kube(params) => { ProtocolParams::Kube(params) => {
info!( let namespace = params.namespace.as_deref().unwrap_or("default");
"Client is not connected to remote; connecting to pod {}", info!("Client is not connected to remote; connecting to namespace {namespace}",);
params.pod, format!("Connecting to Kube namespace {namespace}…",)
);
format!("Connecting to {}…", params.pod)
} }
ProtocolParams::Smb(params) => { ProtocolParams::Smb(params) => {
info!( info!(
+10 -4
View File
@@ -14,6 +14,7 @@ mod view;
// locals // locals
use std::collections::VecDeque; use std::collections::VecDeque;
use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
// Includes // Includes
@@ -21,6 +22,7 @@ use chrono::{DateTime, Local};
use lib::browser; use lib::browser;
use lib::browser::Browser; use lib::browser::Browser;
use lib::transfer::{TransferOpts, TransferStates}; use lib::transfer::{TransferOpts, TransferStates};
use lib::walkdir::WalkdirStates;
use remotefs::RemoteFs; use remotefs::RemoteFs;
use session::TransferPayload; use session::TransferPayload;
use tempfile::TempDir; use tempfile::TempDir;
@@ -50,7 +52,6 @@ enum Id {
FatalPopup, FatalPopup,
FileInfoPopup, FileInfoPopup,
FilterPopup, FilterPopup,
FindPopup,
FooterBar, FooterBar,
GlobalListener, GlobalListener,
GotoPopup, GotoPopup,
@@ -94,6 +95,7 @@ enum PendingActionMsg {
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum TransferMsg { enum TransferMsg {
AbortWalkdir,
AbortTransfer, AbortTransfer,
Chmod(remotefs::fs::UnixPex), Chmod(remotefs::fs::UnixPex),
CopyFileTo(String), CopyFileTo(String),
@@ -104,6 +106,7 @@ enum TransferMsg {
GoTo(String), GoTo(String),
GoToParentDirectory, GoToParentDirectory,
GoToPreviousDirectory, GoToPreviousDirectory,
InitFuzzySearch,
Mkdir(String), Mkdir(String),
NewFile(String), NewFile(String),
OpenFile, OpenFile,
@@ -111,8 +114,8 @@ enum TransferMsg {
OpenTextFile, OpenTextFile,
ReloadDir, ReloadDir,
RenameFile(String), RenameFile(String),
RescanGotoFiles(PathBuf),
SaveFileAs(String), SaveFileAs(String),
SearchFile(String),
ToggleWatch, ToggleWatch,
ToggleWatchFor(usize), ToggleWatchFor(usize),
TransferFile, TransferFile,
@@ -133,7 +136,6 @@ enum UiMsg {
CloseFileSortingPopup, CloseFileSortingPopup,
CloseFilterPopup, CloseFilterPopup,
CloseFindExplorer, CloseFindExplorer,
CloseFindPopup,
CloseGotoPopup, CloseGotoPopup,
CloseKeybindingsPopup, CloseKeybindingsPopup,
CloseMkdirPopup, CloseMkdirPopup,
@@ -147,6 +149,7 @@ enum UiMsg {
CloseWatcherPopup, CloseWatcherPopup,
Disconnect, Disconnect,
FilterFiles(String), FilterFiles(String),
FuzzySearch(String),
LogBackTabbed, LogBackTabbed,
Quit, Quit,
ReplacePopupTabbed, ReplacePopupTabbed,
@@ -158,7 +161,6 @@ enum UiMsg {
ShowFileInfoPopup, ShowFileInfoPopup,
ShowFileSortingPopup, ShowFileSortingPopup,
ShowFilterPopup, ShowFilterPopup,
ShowFindPopup,
ShowGotoPopup, ShowGotoPopup,
ShowKeybindingsPopup, ShowKeybindingsPopup,
ShowLogPanel, ShowLogPanel,
@@ -219,6 +221,9 @@ pub struct FileTransferActivity {
browser: Browser, browser: Browser,
/// Current log lines /// Current log lines
log_records: VecDeque<LogRecord>, log_records: VecDeque<LogRecord>,
/// Fuzzy search states
walkdir: WalkdirStates,
/// Transfer states
transfer: TransferStates, transfer: TransferStates,
/// Temporary directory where to store temporary stuff /// Temporary directory where to store temporary stuff
cache: Option<TempDir>, cache: Option<TempDir>,
@@ -246,6 +251,7 @@ impl FileTransferActivity {
client: Builder::build(params.protocol, params.params.clone(), &config_client), client: Builder::build(params.protocol, params.params.clone(), &config_client),
browser: Browser::new(&config_client), browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
walkdir: WalkdirStates::default(),
transfer: TransferStates::default(), transfer: TransferStates::default(),
cache: match TempDir::new() { cache: match TempDir::new() {
Ok(d) => Some(d), Ok(d) => Some(d),
+49 -26
View File
@@ -57,7 +57,11 @@ impl FileTransferActivity {
// Connect to remote // Connect to remote
match self.client.connect() { match self.client.connect() {
Ok(Welcome { banner, .. }) => { Ok(Welcome { banner, .. }) => {
self.connected = true; self.connected = self.client.is_connected();
if !self.connected {
return;
}
if let Some(banner) = banner { if let Some(banner) = banner {
// Log welcome // Log welcome
self.log( self.log(
@@ -68,6 +72,15 @@ impl FileTransferActivity {
banner banner
), ),
); );
} else {
// Log welcome
self.log(
LogLevel::Info,
format!(
"Established connection with '{}'",
self.get_remote_hostname()
),
);
} }
// Try to change directory to entry directory // Try to change directory to entry directory
let mut remote_chdir: Option<PathBuf> = None; let mut remote_chdir: Option<PathBuf> = None;
@@ -87,7 +100,7 @@ impl FileTransferActivity {
Err(err) => { Err(err) => {
// Set popup fatal error // Set popup fatal error
self.umount_wait(); 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 /// Reload remote directory entries and update browser
pub(super) fn reload_remote_dir(&mut self) { pub(super) fn reload_remote_dir(&mut self) {
if !self.connected {
return;
}
// Get current entries // Get current entries
if let Ok(wrkdir) = self.client.pwd() { if let Ok(wrkdir) = self.client.pwd() {
self.mount_blocking_wait("Loading remote directory..."); self.mount_blocking_wait("Loading remote directory...");
if self.remote_scan(wrkdir.as_path()).is_ok() { let res = self.remote_scan(wrkdir.as_path());
// Set wrkdir
self.remote_mut().wrkdir = wrkdir;
}
self.umount_wait(); 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(); let wrkdir: PathBuf = self.host.pwd();
if self.local_scan(wrkdir.as_path()).is_ok() { let res = self.local_scan(wrkdir.as_path());
self.local_mut().wrkdir = wrkdir;
}
self.umount_wait(); 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 /// Scan current local directory
fn local_scan(&mut self, path: &Path) -> Result<(), HostError> { fn local_scan(&mut self, path: &Path) -> Result<(), HostError> {
match self.host.scan_dir(path) { match self.host.list_dir(path) {
Ok(files) => { Ok(files) => {
// Set files and sort (sorting is implicit) // Set files and sort (sorting is implicit)
self.local_mut().set_files(files); self.local_mut().set_files(files);
Ok(()) Ok(())
} }
Err(err) => { Err(err) => Err(err),
self.log_and_alert(
LogLevel::Error,
format!("Could not scan current directory: {err}"),
);
Err(err)
}
} }
} }
@@ -164,13 +193,7 @@ impl FileTransferActivity {
self.remote_mut().set_files(files); self.remote_mut().set_files(files);
Ok(()) Ok(())
} }
Err(err) => { Err(err) => Err(err),
self.log_and_alert(
LogLevel::Error,
format!("Could not scan current directory: {err}"),
);
Err(err)
}
} }
} }
@@ -335,7 +358,7 @@ impl FileTransferActivity {
} }
} }
// Get files in dir // Get files in dir
match self.host.scan_dir(entry.path()) { match self.host.list_dir(entry.path()) {
Ok(entries) => { Ok(entries) => {
// Iterate over files // Iterate over files
for entry in entries.iter() { for entry in entries.iter() {
@@ -1133,7 +1156,7 @@ impl FileTransferActivity {
fn get_total_transfer_size_local(&mut self, entry: &File) -> usize { fn get_total_transfer_size_local(&mut self, entry: &File) -> usize {
if entry.is_dir() { if entry.is_dir() {
// List dir // List dir
match self.host.scan_dir(entry.path()) { match self.host.list_dir(entry.path()) {
Ok(files) => files Ok(files) => files
.iter() .iter()
.map(|x| self.get_total_transfer_size_local(x)) .map(|x| self.get_total_transfer_size_local(x))
+72 -54
View File
@@ -8,6 +8,7 @@ use remotefs::fs::File;
use tuirealm::props::{AttrValue, Attribute}; use tuirealm::props::{AttrValue, Attribute};
use tuirealm::{State, StateValue, Update}; use tuirealm::{State, StateValue, Update};
use super::actions::walkdir::WalkdirError;
use super::actions::SelectedFile; use super::actions::SelectedFile;
use super::browser::{FileExplorerTab, FoundExplorerTab}; use super::browser::{FileExplorerTab, FoundExplorerTab};
use super::{ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg}; use super::{ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg};
@@ -32,6 +33,9 @@ impl FileTransferActivity {
TransferMsg::AbortTransfer => { TransferMsg::AbortTransfer => {
self.transfer.abort(); self.transfer.abort();
} }
TransferMsg::AbortWalkdir => {
self.walkdir.aborted = true;
}
TransferMsg::Chmod(mode) => { TransferMsg::Chmod(mode) => {
self.umount_chmod(); self.umount_chmod();
self.mount_blocking_wait("Applying new file mode…"); 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) => { TransferMsg::Mkdir(dir) => {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_mkdir(dir), FileExplorerTab::Local => self.action_local_mkdir(dir),
@@ -267,6 +324,15 @@ impl FileTransferActivity {
// Reload files // Reload files
self.update_browser_file_list() 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) => { TransferMsg::SaveFileAs(dest) => {
self.umount_saveas(); self.umount_saveas();
match self.browser.tab() { match self.browser.tab() {
@@ -281,57 +347,7 @@ impl FileTransferActivity {
// Reload files // Reload files
self.update_browser_file_list_swapped(); 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::ToggleWatch => self.action_toggle_watch(),
TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index), TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index),
TransferMsg::TransferFile => { TransferMsg::TransferFile => {
@@ -405,7 +421,6 @@ impl FileTransferActivity {
self.finalize_find(); self.finalize_find();
self.umount_find(); self.umount_find();
} }
UiMsg::CloseFindPopup => self.umount_find_input(),
UiMsg::CloseGotoPopup => self.umount_goto(), UiMsg::CloseGotoPopup => self.umount_goto(),
UiMsg::CloseKeybindingsPopup => self.umount_help(), UiMsg::CloseKeybindingsPopup => self.umount_help(),
UiMsg::CloseMkdirPopup => self.umount_mkdir(), UiMsg::CloseMkdirPopup => self.umount_mkdir(),
@@ -439,7 +454,7 @@ impl FileTransferActivity {
wrkdir.as_path(), wrkdir.as_path(),
); );
// Mount result widget // Mount result widget
self.mount_find(&filter); self.mount_find(&filter, false);
self.update_find_list(); self.update_find_list();
// Initialize tab // Initialize tab
self.browser.change_tab(match self.browser.tab() { self.browser.change_tab(match self.browser.tab() {
@@ -448,6 +463,10 @@ impl FileTransferActivity {
_ => FileExplorerTab::FindLocal, _ => FileExplorerTab::FindLocal,
}); });
} }
UiMsg::FuzzySearch(needle) => {
self.browser.fuzzy_search(&needle);
self.update_find_list();
}
UiMsg::ShowLogPanel => { UiMsg::ShowLogPanel => {
assert!(self.app.active(&Id::Log).is_ok()); assert!(self.app.active(&Id::Log).is_ok());
} }
@@ -514,7 +533,6 @@ impl FileTransferActivity {
} }
UiMsg::ShowFileSortingPopup => self.mount_file_sorting(), UiMsg::ShowFileSortingPopup => self.mount_file_sorting(),
UiMsg::ShowFilterPopup => self.mount_filter(), UiMsg::ShowFilterPopup => self.mount_filter(),
UiMsg::ShowFindPopup => self.mount_find_input(),
UiMsg::ShowGotoPopup => self.mount_goto(), UiMsg::ShowGotoPopup => self.mount_goto(),
UiMsg::ShowKeybindingsPopup => self.mount_help(), UiMsg::ShowKeybindingsPopup => self.mount_help(),
UiMsg::ShowMkdirPopup => self.mount_mkdir(), UiMsg::ShowMkdirPopup => self.mount_mkdir(),
+98 -51
View File
@@ -6,12 +6,14 @@
// Ext // Ext
use remotefs::fs::{File, UnixPex}; use remotefs::fs::{File, UnixPex};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{PropPayload, PropValue, TextSpan};
use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::tui::widgets::Clear; use tuirealm::tui::widgets::Clear;
use tuirealm::{Sub, SubClause, SubEventClause}; use tuirealm::{AttrValue, Attribute, Sub, SubClause, SubEventClause};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use super::browser::{FileExplorerTab, FoundExplorerTab}; use super::browser::{FileExplorerTab, FoundExplorerTab};
use super::components::ATTR_FILES;
use super::{components, Context, FileTransferActivity, Id}; use super::{components, Context, FileTransferActivity, Id};
use crate::explorer::FileSorting; use crate::explorer::FileSorting;
use crate::utils::ui::{Popup, Size}; use crate::utils::ui::{Popup, Size};
@@ -80,7 +82,7 @@ impl FileTransferActivity {
self.refresh_remote_status_bar(); self.refresh_remote_status_bar();
// Update components // Update components
self.update_local_filelist(); self.update_local_filelist();
self.update_remote_filelist(); // self.update_remote_filelist();
// Global listener // Global listener
self.mount_global_listener(); self.mount_global_listener();
// Give focus to local explorer // Give focus to local explorer
@@ -177,11 +179,6 @@ impl FileTransferActivity {
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::FilterPopup, f, 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) { } else if self.app.mounted(&Id::GotoPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
@@ -307,7 +304,15 @@ impl FileTransferActivity {
// make popup // make popup
self.app.view(&Id::ErrorPopup, f, popup); self.app.view(&Id::ErrorPopup, f, popup);
} else if self.app.mounted(&Id::WaitPopup) { } 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); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::WaitPopup, f, popup); self.app.view(&Id::WaitPopup, f, popup);
@@ -397,6 +402,38 @@ impl FileTransferActivity {
assert!(self.app.active(&Id::WaitPopup).is_ok()); 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) { pub(super) fn mount_blocking_wait<S: AsRef<str>>(&mut self, text: S) {
self.mount_wait(text); self.mount_wait(text);
self.view(); self.view();
@@ -515,7 +552,7 @@ impl FileTransferActivity {
let _ = self.app.umount(&Id::ExecPopup); 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 // Get color
let (bg, fg, hg) = match self.browser.tab() { let (bg, fg, hg) = match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => ( FileExplorerTab::Local | FileExplorerTab::FindLocal => (
@@ -529,18 +566,29 @@ impl FileTransferActivity {
self.theme().transfer_remote_explorer_highlighted, self.theme().transfer_remote_explorer_highlighted,
), ),
}; };
// Mount component // Mount component
assert!(self assert!(self
.app .app
.remount( .remount(
Id::ExplorerFind, Id::ExplorerFind,
Box::new(components::ExplorerFind::new( if fuzzy_search {
format!(r#"Search results for "{search}""#), Box::new(components::ExplorerFuzzy::new(
&[], msg.to_string(),
bg, &[],
fg, bg,
hg fg,
)), hg,
))
} else {
Box::new(components::ExplorerFind::new(
msg.to_string(),
&[],
bg,
fg,
hg,
))
},
vec![], vec![],
) )
.is_ok()); .is_ok());
@@ -551,37 +599,41 @@ impl FileTransferActivity {
let _ = self.app.umount(&Id::ExplorerFind); 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) { 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; let input_color = self.theme().misc_input_dialog;
assert!(self assert!(self
.app .app
.remount( .remount(
Id::GotoPopup, Id::GotoPopup,
Box::new(components::GoToPopup::new(input_color)), Box::new(components::GotoPopup::new(input_color, files)),
vec![], vec![],
) )
.is_ok()); .is_ok());
assert!(self.app.active(&Id::GotoPopup).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) { pub(super) fn umount_goto(&mut self) {
let _ = self.app.umount(&Id::GotoPopup); let _ = self.app.umount(&Id::GotoPopup);
} }
@@ -1094,38 +1146,33 @@ impl FileTransferActivity {
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SortingPopup, Id::SortingPopup,
)))), )))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::FindPopup,
)))),
Box::new(SubClause::And( Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SyncBrowsingMkdirPopup, Id::SyncBrowsingMkdirPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SymlinkPopup,
)))), )))),
Box::new(SubClause::And( Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SymlinkPopup, Id::WatcherPopup,
)))), )))),
Box::new(SubClause::And( Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WatcherPopup, Id::WatchedPathsList,
)))), )))),
Box::new(SubClause::And( Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WatchedPathsList, Id::ChmodPopup,
)))), )))),
Box::new(SubClause::And( Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( 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,
)))),
)),
)), )),
)), )),
)), )),
+9 -13
View File
@@ -124,7 +124,7 @@ impl SetupActivity {
} }
/// Create a new ssh key /// 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 // get parameters
let host: String = match self.app.state(&Id::Ssh(IdSsh::SshHost)) { let host: String = match self.app.state(&Id::Ssh(IdSsh::SshHost)) {
Ok(State::One(StateValue::String(host))) => host, Ok(State::One(StateValue::String(host))) => host,
@@ -148,29 +148,23 @@ impl SetupActivity {
// Lock ports // Lock ports
assert!(self.app.lock_ports().is_ok()); assert!(self.app.lock_ports().is_ok());
// Write key to file // Write key to file
match edit::edit(placeholder.as_bytes()) { let res = match edit::edit(placeholder.as_bytes()) {
Ok(rsa_key) => { Ok(rsa_key) => {
// Remove placeholder from `rsa_key` // Remove placeholder from `rsa_key`
let rsa_key: String = rsa_key.as_str().replace(placeholder.as_str(), ""); let rsa_key: String = rsa_key.as_str().replace(placeholder.as_str(), "");
if rsa_key.is_empty() { if rsa_key.is_empty() {
// Report error: empty key // Report error: empty key
self.mount_error("SSH key is empty!"); Err("SSH key is empty!".to_string())
} else { } else {
// Add key self.add_ssh_key(host.as_str(), username.as_str(), rsa_key.as_str())
if let Err(err) = .map_err(|e| format!("Could not create new private key: {e}"))
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(),
);
}
} }
} }
Err(err) => { Err(err) => {
// Report error // 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 // Restore terminal
if let Some(ctx) = self.context.as_mut() { if let Some(ctx) = self.context.as_mut() {
// Enter alternate mode // Enter alternate mode
@@ -187,6 +181,8 @@ impl SetupActivity {
// Unlock ports // Unlock ports
assert!(self.app.unlock_ports().is_ok()); assert!(self.app.unlock_ports().is_ok());
} }
res
} }
/// Given a component and a color, save the color into the theme /// Given a component and a color, save the color into the theme
+7 -2
View File
@@ -225,9 +225,14 @@ impl SetupActivity {
} }
} }
SshMsg::SaveSshKey => { SshMsg::SaveSshKey => {
self.action_new_ssh_key(); let res = self.action_new_ssh_key();
self.umount_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 => { SshMsg::ShowDelSshKeyPopup => {
self.mount_del_ssh_key(); self.mount_del_ssh_key();
+8 -3
View File
@@ -30,7 +30,7 @@ impl Context {
theme_provider: ThemeProvider, theme_provider: ThemeProvider,
error: Option<String>, error: Option<String>,
) -> Context { ) -> Context {
Context { let mut ctx = Context {
bookmarks_client, bookmarks_client,
config_client, config_client,
ft_params: None, ft_params: None,
@@ -38,7 +38,13 @@ impl Context {
terminal: TerminalBridge::new().expect("Could not initialize terminal"), terminal: TerminalBridge::new().expect("Could not initialize terminal"),
theme_provider, theme_provider,
error, error,
} };
// Init terminal state
let _ = ctx.terminal.enable_raw_mode();
let _ = ctx.terminal.enter_alternate_screen();
ctx
} }
// -- getters // -- getters
@@ -107,6 +113,5 @@ impl Drop for Context {
// Re-enable terminal stuff // Re-enable terminal stuff
let _ = self.terminal.disable_raw_mode(); let _ = self.terminal.disable_raw_mode();
let _ = self.terminal.leave_alternate_screen(); let _ = self.terminal.leave_alternate_screen();
let _ = self.terminal.clear_screen();
} }
} }
+16 -25
View File
@@ -56,12 +56,12 @@ static REMOTE_WEBDAV_OPT_REGEX: Lazy<Regex> =
lazy_regex!(r"(?:([^:]+):)(?:(.+[^@])@)(?:([^/]+))(?:(.+))?"); lazy_regex!(r"(?:([^:]+):)(?:(.+[^@])@)(?:([^/]+))(?:(.+))?");
/** /**
* Regex matches: {container}@{pod}/{path} * Regex matches: {namespace}[@{cluster_url}]$/{path}
* - group 1: Container * - group 1: Namespace
* - group 2: Pod * - group 3: Some(cluster_url) | None
* - group 3: Some(path) | 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: * Regex matches:
@@ -100,8 +100,7 @@ static REMOTE_SMB_OPT_REGEX: Lazy<Regex> =
/** /**
* Regex matches: * Regex matches:
* - group 1: Version * - group 1: Version
* E.g. termscp-0.3.2 => 0.3.2 * E.g. termscp-0.3.2 => 0.3.2; v0.4.0 => 0.4.0
* v0.4.0 => 0.4.0
*/ */
static SEMVER_REGEX: Lazy<Regex> = lazy_regex!(r".*(:?[0-9]\.[0-9]\.[0-9])"); 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> { fn parse_kube_remote_opt(s: &str) -> Result<FileTransferParams, String> {
match REMOTE_KUBE_OPT_REGEX.captures(s) { match REMOTE_KUBE_OPT_REGEX.captures(s) {
Some(groups) => { Some(groups) => {
let container: String = groups let namespace: Option<String> = groups.get(1).map(|x| x.as_str().to_string());
.get(1) let cluster_url: Option<String> = groups.get(3).map(|x| x.as_str().to_string());
.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 remote_path: Option<PathBuf> = 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( Ok(FileTransferParams::new(
FileTransferProtocol::Kube, FileTransferProtocol::Kube,
ProtocolParams::Kube(KubeProtocolParams { ProtocolParams::Kube(KubeProtocolParams {
pod, namespace,
container, cluster_url,
namespace: None,
cluster_url: None,
username: None, username: None,
client_cert: None, client_cert: None,
client_key: None, client_key: None,
@@ -754,13 +745,13 @@ mod tests {
#[test] #[test]
fn should_parse_kube_address() { 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(); let params = result.params.kube_params().unwrap();
assert_eq!(params.container.as_str(), "alpine"); assert_eq!(params.namespace, Some("my-namespace".to_string()));
assert_eq!(params.pod.as_str(), "my-pod"); assert_eq!(params.cluster_url.as_deref(), Some("http://localhost:1234"));
assert_eq!(params.namespace, None);
assert_eq!(params.cluster_url, None);
assert_eq!(params.username, None); assert_eq!(params.username, None);
assert_eq!(params.client_cert, None); assert_eq!(params.client_cert, None);
assert_eq!(params.client_key, None); assert_eq!(params.client_key, None);