mirror of
https://github.com/veeso/termscp.git
synced 2026-09-27 06:21:22 -07:00
Merge pull request #288 from veeso/0.15
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 }}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+33
-33
@@ -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]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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>
|
<span translate="intro.versionAlert">termscp 0.15.0 is NOW out! Download it from</span>
|
||||||
<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
@@ -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
@@ -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
@@ -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
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(¶ms.pod, ¶ms.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()),
|
||||||
|
|||||||
@@ -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
@@ -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() {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(¶ms.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(""));
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)))),
|
|
||||||
)),
|
|
||||||
)),
|
)),
|
||||||
)),
|
)),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user