mirror of
https://github.com/veeso/termscp.git
synced 2025-12-06 17:15:35 -08:00
Merge pull request #288 from veeso/0.15
This commit is contained in:
10
.github/workflows/build-artifacts.yml
vendored
10
.github/workflows/build-artifacts.yml
vendored
@@ -22,21 +22,21 @@ jobs:
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
target: ${{ matrix.platform.target }}
|
||||
targets: ${{ matrix.platform.target }}
|
||||
- name: Build release
|
||||
run: cargo build --release --target ${{ matrix.platform.target }}
|
||||
- name: Prepare artifact files
|
||||
run: |
|
||||
mkdir -p .artifact
|
||||
mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp
|
||||
ls -l .artifact/
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
name: ${{ matrix.platform.release_for }}
|
||||
path: .artifact/*
|
||||
path: .artifact/termscp
|
||||
|
||||
45
.github/workflows/coverage.yml
vendored
45
.github/workflows/coverage.yml
vendored
@@ -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 }}
|
||||
3
.github/workflows/linux.yml
vendored
3
.github/workflows/linux.yml
vendored
@@ -21,10 +21,9 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1
|
||||
|
||||
3
.github/workflows/macos.yml
vendored
3
.github/workflows/macos.yml
vendored
@@ -18,10 +18,9 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Build
|
||||
run: cargo build
|
||||
|
||||
3
.github/workflows/windows.yml
vendored
3
.github/workflows/windows.yml
vendored
@@ -19,10 +19,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Build
|
||||
run: cargo build
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,6 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
- [Changelog](#changelog)
|
||||
- [0.15.0](#0150)
|
||||
- [0.14.0](#0140)
|
||||
- [0.13.0](#0130)
|
||||
- [0.12.3](#0123)
|
||||
@@ -36,6 +37,20 @@
|
||||
|
||||
---
|
||||
|
||||
## 0.15.0
|
||||
|
||||
Released on 03/10/2024
|
||||
|
||||
- [Issue 249](https://github.com/veeso/termscp/issues/249): The old *find* command has been replaced with a brand new explorer with support to 🪄 **Fuzzy search** 🪄. The command is still `<F>`.
|
||||
- [Issue 283](https://github.com/veeso/termscp/issues/283): **Find command can now be cancelled** by pressing `<CTRL+C>`. While scanning the directory it will also display the current progress.
|
||||
- [Issue 268](https://github.com/veeso/termscp/issues/268): 📦 **Pods and container explorer** 🐳 for Kube protocol.
|
||||
- BREAKING ‼️ Kube address argument has changed to `namespace[@<cluster_url>][$<path>]`
|
||||
- Pod and container argumets have been removed; from now on you will connect with the following syntax to the provided namespace: `/pod-name/container-name/path/to/file`
|
||||
- [Issue 279](https://github.com/veeso/termscp/issues/279): do not clear screen
|
||||
- [Issue 277](https://github.com/veeso/termscp/issues/277): Fix a bug in the configuration page, which caused being stuck if the added SSH key was empty
|
||||
- [Issue 272](https://github.com/veeso/termscp/issues/272): `isolated-tests` feature to run tests for releasing on distributions which run in isolated environments
|
||||
- [Issue 280](https://github.com/veeso/termscp/issues/280): Autocompletion when pressing `<TAB>` on the `Go to` popup.
|
||||
|
||||
## 0.14.0
|
||||
|
||||
Released on 17/07/2024
|
||||
|
||||
2544
Cargo.lock
generated
2544
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
66
Cargo.toml
66
Cargo.toml
@@ -5,18 +5,12 @@ description = "termscp is a feature rich terminal file transfer and explorer wit
|
||||
edition = "2021"
|
||||
homepage = "https://termscp.veeso.dev"
|
||||
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
keywords = [
|
||||
"scp-client",
|
||||
"sftp-client",
|
||||
"ftp-client",
|
||||
"winscp",
|
||||
"command-line-utility",
|
||||
]
|
||||
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
|
||||
license = "MIT"
|
||||
name = "termscp"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
|
||||
[package.metadata.rpm]
|
||||
package = "termscp"
|
||||
@@ -38,32 +32,37 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
argh = "^0.1"
|
||||
bitflags = "^2.1"
|
||||
bytesize = "^1.1"
|
||||
bitflags = "^2"
|
||||
bytesize = "^1"
|
||||
chrono = "^0.4"
|
||||
content_inspector = "^0.2"
|
||||
dirs = "^5.0"
|
||||
edit = "^0.1"
|
||||
filetime = "^0.2"
|
||||
hostname = "^0.4"
|
||||
keyring = { version = "^2.0", optional = true }
|
||||
lazy-regex = "^3.1"
|
||||
lazy_static = "^1.4"
|
||||
keyring = { version = "^3", optional = true, features = [
|
||||
"apple-native",
|
||||
"windows-native",
|
||||
"sync-secret-service",
|
||||
] }
|
||||
lazy-regex = "^3"
|
||||
lazy_static = "^1"
|
||||
log = "^0.4"
|
||||
magic-crypt = "^3.1"
|
||||
notify = "=4.0.17"
|
||||
magic-crypt = "^3"
|
||||
notify = "6"
|
||||
notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
|
||||
nucleo = "0.5"
|
||||
open = "^5.0"
|
||||
rand = "^0.8.5"
|
||||
regex = "^1"
|
||||
remotefs = "^0.2.0"
|
||||
remotefs-aws-s3 = { version = "^0.2.4", default-features = false, features = [
|
||||
remotefs = "^0.3"
|
||||
remotefs-aws-s3 = { version = "^0.3", default-features = false, features = [
|
||||
"find",
|
||||
"rustls",
|
||||
] }
|
||||
remotefs-kube = "0.2"
|
||||
remotefs-webdav = "^0.1.1"
|
||||
rpassword = "^7.0"
|
||||
remotefs-kube = "0.4"
|
||||
remotefs-webdav = "^0.2"
|
||||
rpassword = "^7"
|
||||
self_update = { version = "^0.41", default-features = false, features = [
|
||||
"rustls",
|
||||
"archive-tar",
|
||||
@@ -74,19 +73,19 @@ self_update = { version = "^0.41", default-features = false, features = [
|
||||
serde = { version = "^1", features = ["derive"] }
|
||||
simplelog = "^0.12"
|
||||
ssh2-config = "^0.2"
|
||||
tempfile = "^3.4"
|
||||
tempfile = "^3"
|
||||
thiserror = "^1"
|
||||
tokio = { version = "=1.38.1", features = ["rt"] }
|
||||
toml = "^0.8"
|
||||
tui-realm-stdlib = "^1.3.1"
|
||||
tuirealm = "^1.9.1"
|
||||
unicode-width = "^0.1"
|
||||
tui-realm-stdlib = "^1.3"
|
||||
tuirealm = "^1.9"
|
||||
unicode-width = "^0.2"
|
||||
version-compare = "^0.2"
|
||||
whoami = "^1.4"
|
||||
wildmatch = "^2.1"
|
||||
whoami = "^1.5"
|
||||
wildmatch = "^2"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "^1.3"
|
||||
pretty_assertions = "^1"
|
||||
serial_test = "^3"
|
||||
|
||||
[build-dependencies]
|
||||
@@ -95,21 +94,22 @@ cfg_aliases = "0.2"
|
||||
[features]
|
||||
default = ["smb", "with-keyring"]
|
||||
github-actions = []
|
||||
with-keyring = ["keyring"]
|
||||
isolated-tests = []
|
||||
smb = ["remotefs-smb"]
|
||||
with-keyring = ["keyring"]
|
||||
|
||||
[target."cfg(not(target_os = \"macos\"))".dependencies]
|
||||
remotefs-smb = { version = "^0.2", optional = true }
|
||||
remotefs-smb = { version = "^0.3", optional = true }
|
||||
|
||||
[target."cfg(target_family = \"windows\")"]
|
||||
[target."cfg(target_family = \"windows\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.1.2", features = ["native-tls"] }
|
||||
remotefs-ssh = "^0.3.1"
|
||||
remotefs-ftp = { version = "^0.2", features = ["native-tls"] }
|
||||
remotefs-ssh = "^0.4"
|
||||
|
||||
[target."cfg(target_family = \"unix\")"]
|
||||
[target."cfg(target_family = \"unix\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.1.2", features = ["vendored", "native-tls"] }
|
||||
remotefs-ssh = { version = "^0.3.1", features = ["ssh2-vendored"] }
|
||||
remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
|
||||
remotefs-ssh = { version = "^0.4", features = ["ssh2-vendored"] }
|
||||
users = "0.11.0"
|
||||
|
||||
[profile.dev]
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Developed by <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Current version: 0.14.0 (17/07/2024)</p>
|
||||
<p align="center">Current version: 0.15.0 (03/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -116,11 +116,6 @@
|
||||
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
|
||||
alt="Windows CI"
|
||||
/></a>
|
||||
<a href="https://coveralls.io/github/veeso/termscp"
|
||||
><img
|
||||
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
|
||||
alt="Coveralls"
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Entwickelt von <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Aktuelle Version: 0.14.0 (17/07/2024)</p>
|
||||
<p align="center">Aktuelle Version: 0.15.0 (03/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -133,7 +133,7 @@ s3://buckethead@eu-central-1:default:/assets
|
||||
Falls Sie eine Verbindung zu Kube herstellen möchten, verwenden Sie die folgende Syntax
|
||||
|
||||
```txt
|
||||
kube://<container>@<pod></path>
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### SMB Adressargument
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Desarrollado por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versión actual: 0.14.0 (17/07/2024)</p>
|
||||
<p align="center">Versión actual: 0.15.0 (03/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -110,7 +110,7 @@ s3://buckethead@eu-central-1:default:/assets
|
||||
En caso de que quieras conectarte a Kube, utiliza la siguiente sintaxis
|
||||
|
||||
```txt
|
||||
kube://<container>@<pod></path>
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### Argumento de dirección de WebDAV
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Développé par <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Version actuelle: 0.14.0 (17/07/2024)</p>
|
||||
<p align="center">Version actuelle: 0.15.0 (03/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -108,7 +108,7 @@ s3://buckethead@eu-central-1:default:/assets
|
||||
Si vous souhaitez vous connecter à Kube, utilisez la syntaxe suivante
|
||||
|
||||
```txt
|
||||
kube://<container>@<pod></path>
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### Argument d'adresse WebDAV
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Sviluppato da <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versione corrente: 0.14.0 (17/07/2024)</p>
|
||||
<p align="center">Versione corrente: 0.15.0 (03/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -106,7 +106,7 @@ s3://buckethead@eu-central-1:default:/assets
|
||||
Nel caso tu voglia connetterti a Kube usa la seguente sintassi
|
||||
|
||||
```txt
|
||||
kube://<container>@<pod></path>
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### Argomento indirizzo per WebDAV
|
||||
|
||||
@@ -111,7 +111,7 @@ s3://buckethead@eu-central-1:default:/assets
|
||||
In case you want to connect to Kube use the following syntax
|
||||
|
||||
```txt
|
||||
kube://<container>@<pod></path>
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### WebDAV address argument
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Desenvolvido por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versão atual: 0.14.0 (17/07/2024)</p>
|
||||
<p align="center">Versão atual: 0.15.0 (03/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -111,7 +111,7 @@ s3://buckethead@eu-central-1:default:/assets
|
||||
Caso queira se conectar ao Kube, use a seguinte sintaxe
|
||||
|
||||
```txt
|
||||
kube://<container>@<pod></path>
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### Argumento de Endereço do WebDAV
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">由 <a href="https://veeso.dev/" target="_blank">@veeso</a> 开发</p>
|
||||
<p align="center">当前版本: 0.14.0 (17/07/2024)</p>
|
||||
<p align="center">当前版本: 0.15.0 (03/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -108,7 +108,7 @@ s3://buckethead@eu-central-1:default:/assets
|
||||
如果您想连接到 Kube,请使用以下语法
|
||||
|
||||
```txt
|
||||
kube://<container>@<pod></path>
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### WebDAV 地址参数
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# -f, -y, --force, --yes
|
||||
# Skip the confirmation prompt during installation
|
||||
|
||||
TERMSCP_VERSION="0.14.0"
|
||||
TERMSCP_VERSION="0.15.0"
|
||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
||||
since last release, so if the latest version is not available yet,
|
||||
you can install it downloading the ZIP file from</span>
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.14.0.nupkg"
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.15.0.nupkg"
|
||||
target="_blank">Github</a>
|
||||
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
||||
</p>
|
||||
@@ -74,7 +74,7 @@
|
||||
On Debian based distros, you can install termscp using the Deb
|
||||
package via:
|
||||
</p>
|
||||
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.14.0_amd64.deb</span>
|
||||
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.15.0_amd64.deb</span>
|
||||
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
|
||||
</div>
|
||||
<h3>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</button>
|
||||
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
||||
<p class="text-lg">
|
||||
<span translate="intro.versionAlert">termscp 0.14.0 is NOW out! Download it from</span>
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Get started →",
|
||||
"versionAlert": "termscp 0.14.0 is NOW out! Download it from",
|
||||
"versionAlert": "termscp 0.15.0 is NOW out! Download it from",
|
||||
"here": "here",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Para iniciar →",
|
||||
"versionAlert": "termscp 0.14.0 ya está disponible! Descárgalo desde",
|
||||
"versionAlert": "termscp 0.15.0 ya está disponible! Descárgalo desde",
|
||||
"here": "aquì",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Pour commencer →",
|
||||
"versionAlert": "termscp 0.14.0 est maintenant sorti! Télécharge-le depuis",
|
||||
"versionAlert": "termscp 0.15.0 est maintenant sorti! Télécharge-le depuis",
|
||||
"here": "ici",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
||||
"getStarted": "Installa termscp →",
|
||||
"versionAlert": "termscp 0.14.0 è ORA disponbile! Scaricalo da",
|
||||
"versionAlert": "termscp 0.15.0 è ORA disponbile! Scaricalo da",
|
||||
"here": "qui",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "开始 →",
|
||||
"versionAlert": "termscp 0.14.0 现已发布! 从下载",
|
||||
"versionAlert": "termscp 0.15.0 现已发布! 从下载",
|
||||
"here": "这里",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -351,8 +351,6 @@ mod tests {
|
||||
#[test]
|
||||
fn bookmark_from_kube_ftparams() {
|
||||
let params = ProtocolParams::Kube(KubeProtocolParams {
|
||||
pod: "pod".to_string(),
|
||||
container: "container".to_string(),
|
||||
namespace: Some("default".to_string()),
|
||||
username: Some("root".to_string()),
|
||||
cluster_url: Some("https://localhost:6443".to_string()),
|
||||
@@ -368,8 +366,6 @@ mod tests {
|
||||
assert!(bookmark.username.is_none());
|
||||
assert!(bookmark.password.is_none());
|
||||
let kube: &KubeParams = bookmark.kube.as_ref().unwrap();
|
||||
assert_eq!(kube.pod_name.as_str(), "pod");
|
||||
assert_eq!(kube.container.as_str(), "container");
|
||||
assert_eq!(kube.namespace.as_deref().unwrap(), "default");
|
||||
assert_eq!(
|
||||
kube.cluster_url.as_deref().unwrap(),
|
||||
@@ -494,8 +490,6 @@ mod tests {
|
||||
remote_path: Some(PathBuf::from("/tmp")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: Some(KubeParams {
|
||||
pod_name: String::from("pod"),
|
||||
container: String::from("container"),
|
||||
namespace: Some(String::from("default")),
|
||||
cluster_url: Some(String::from("https://localhost:6443")),
|
||||
username: Some(String::from("root")),
|
||||
@@ -516,7 +510,6 @@ mod tests {
|
||||
std::path::Path::new("/usr")
|
||||
);
|
||||
let gparams = params.params.kube_params().unwrap();
|
||||
assert_eq!(gparams.pod.as_str(), "pod");
|
||||
assert_eq!(gparams.namespace.as_deref().unwrap(), "default");
|
||||
assert_eq!(
|
||||
gparams.cluster_url.as_deref().unwrap(),
|
||||
|
||||
@@ -5,8 +5,6 @@ use crate::filetransfer::params::KubeProtocolParams;
|
||||
/// Extra Connection parameters for Kube protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct KubeParams {
|
||||
pub pod_name: String,
|
||||
pub container: String,
|
||||
pub namespace: Option<String>,
|
||||
pub cluster_url: Option<String>,
|
||||
pub username: Option<String>,
|
||||
@@ -17,8 +15,6 @@ pub struct KubeParams {
|
||||
impl From<KubeParams> for KubeProtocolParams {
|
||||
fn from(value: KubeParams) -> Self {
|
||||
Self {
|
||||
pod: value.pod_name,
|
||||
container: value.container,
|
||||
namespace: value.namespace,
|
||||
cluster_url: value.cluster_url,
|
||||
username: value.username,
|
||||
@@ -31,8 +27,6 @@ impl From<KubeParams> for KubeProtocolParams {
|
||||
impl From<KubeProtocolParams> for KubeParams {
|
||||
fn from(value: KubeProtocolParams) -> Self {
|
||||
Self {
|
||||
pod_name: value.pod,
|
||||
container: value.container,
|
||||
namespace: value.namespace,
|
||||
cluster_url: value.cluster_url,
|
||||
username: value.username,
|
||||
|
||||
@@ -412,8 +412,6 @@ mod tests {
|
||||
assert_eq!(host.password, None);
|
||||
assert_eq!(host.protocol, FileTransferProtocol::Kube);
|
||||
let kube = host.kube.as_ref().unwrap();
|
||||
assert_eq!(kube.pod_name.as_str(), "my-pod");
|
||||
assert_eq!(kube.container.as_str(), "my-container");
|
||||
assert_eq!(kube.namespace.as_deref().unwrap(), "my-namespace");
|
||||
assert_eq!(kube.cluster_url.as_deref().unwrap(), "https://my-cluster");
|
||||
assert_eq!(kube.username.as_deref().unwrap(), "my-username");
|
||||
@@ -515,8 +513,6 @@ mod tests {
|
||||
s3: None,
|
||||
smb: None,
|
||||
kube: Some(KubeParams {
|
||||
pod_name: "my-pod".to_string(),
|
||||
container: "my-container".to_string(),
|
||||
namespace: Some("my-namespace".to_string()),
|
||||
cluster_url: Some("https://my-cluster".to_string()),
|
||||
username: Some("my-username".to_string()),
|
||||
@@ -593,6 +589,22 @@ mod tests {
|
||||
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_deserialize_v14_pod_bookmark() {
|
||||
let toml = create_v14_pod_bookmark();
|
||||
toml.as_file().sync_all().unwrap();
|
||||
toml.as_file().rewind().unwrap();
|
||||
let deserialized: UserHosts = deserialize(Box::new(toml)).unwrap();
|
||||
let kube = deserialized.bookmarks.get("pod").unwrap();
|
||||
assert_eq!(kube.protocol, FileTransferProtocol::Kube);
|
||||
let kube = kube.kube.as_ref().unwrap();
|
||||
assert_eq!(kube.namespace.as_deref().unwrap(), "my-namespace");
|
||||
assert_eq!(kube.cluster_url.as_deref().unwrap(), "https://my-cluster");
|
||||
assert_eq!(kube.username.as_deref().unwrap(), "my-username");
|
||||
assert_eq!(kube.client_cert.as_deref().unwrap(), "my-cert");
|
||||
assert_eq!(kube.client_key.as_deref().unwrap(), "my-key");
|
||||
}
|
||||
|
||||
fn create_good_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
@@ -617,8 +629,6 @@ mod tests {
|
||||
[bookmarks.pod]
|
||||
protocol = "KUBE"
|
||||
[bookmarks.pod.kube]
|
||||
pod_name = "my-pod"
|
||||
container = "my-container"
|
||||
namespace = "my-namespace"
|
||||
cluster_url = "https://my-cluster"
|
||||
username = "my-username"
|
||||
@@ -644,6 +654,29 @@ mod tests {
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_v14_pod_bookmark() -> tempfile::NamedTempFile {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
|
||||
[bookmarks.pod]
|
||||
protocol = "KUBE"
|
||||
[bookmarks.pod.kube]
|
||||
pod_name = "my-pod"
|
||||
container = "my-container"
|
||||
namespace = "my-namespace"
|
||||
cluster_url = "https://my-cluster"
|
||||
username = "my-username"
|
||||
client_cert = "my-cert"
|
||||
client_key = "my-key"
|
||||
|
||||
[recents]
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
//write!(tmpfile, "[bookmarks]\nraspberrypi2 = {{ address = \"192.168.1.31\", port = 22, protocol = \"SFTP\", username = \"root\" }}\nmsi-estrem = {{ address = \"192.168.1.30\", port = 22, protocol = \"SFTP\", username = \"cvisintin\" }}\naws-server-prod1 = {{ address = \"51.23.67.12\", port = 21, protocol = \"FTPS\", username = \"aws001\" }}\n\n[recents]\nISO20201215T094000Z = {{ address = \"172.16.104.10\", port = 22, protocol = \"SCP\", username = \"root\" }}\n");
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_bad_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
|
||||
@@ -30,6 +30,7 @@ pub enum FileSorting {
|
||||
ModifyTime,
|
||||
CreationTime,
|
||||
Size,
|
||||
None,
|
||||
}
|
||||
|
||||
/// GroupDirs defines how directories should be grouped in sorting files
|
||||
@@ -178,6 +179,7 @@ impl FileExplorer {
|
||||
FileSorting::CreationTime => self.sort_files_by_creation_time(),
|
||||
FileSorting::ModifyTime => self.sort_files_by_mtime(),
|
||||
FileSorting::Size => self.sort_files_by_size(),
|
||||
FileSorting::None => {}
|
||||
}
|
||||
// Directories first (NOTE: MUST COME AFTER OTHER SORTING)
|
||||
// Group directories if necessary
|
||||
@@ -245,6 +247,7 @@ impl std::fmt::Display for FileSorting {
|
||||
FileSorting::ModifyTime => "by_mtime",
|
||||
FileSorting::Name => "by_name",
|
||||
FileSorting::Size => "by_size",
|
||||
FileSorting::None => "none",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::sync::Arc;
|
||||
use remotefs::RemoteFs;
|
||||
use remotefs_aws_s3::AwsS3Fs;
|
||||
use remotefs_ftp::FtpFs;
|
||||
use remotefs_kube::KubeFs;
|
||||
use remotefs_kube::KubeMultiPodFs as KubeFs;
|
||||
#[cfg(smb_unix)]
|
||||
use remotefs_smb::SmbOptions;
|
||||
#[cfg(smb)]
|
||||
@@ -119,7 +119,7 @@ impl Builder {
|
||||
.build()
|
||||
.expect("Unable to create tokio runtime"),
|
||||
);
|
||||
let kube_fs = KubeFs::new(¶ms.pod, ¶ms.container, &rt);
|
||||
let kube_fs = KubeFs::new(&rt);
|
||||
if let Some(config) = params.config() {
|
||||
kube_fs.config(config)
|
||||
} else {
|
||||
@@ -281,8 +281,6 @@ mod test {
|
||||
#[test]
|
||||
fn test_should_build_kube_fs() {
|
||||
let params = ProtocolParams::Kube(KubeProtocolParams {
|
||||
pod: "pod".to_string(),
|
||||
container: "container".to_string(),
|
||||
namespace: Some("namespace".to_string()),
|
||||
cluster_url: Some("cluster_url".to_string()),
|
||||
username: Some("username".to_string()),
|
||||
|
||||
@@ -3,8 +3,6 @@ use remotefs_kube::Config;
|
||||
/// Protocol params used by WebDAV
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KubeProtocolParams {
|
||||
pub pod: String,
|
||||
pub container: String,
|
||||
pub namespace: Option<String>,
|
||||
pub cluster_url: Option<String>,
|
||||
pub username: Option<String>,
|
||||
|
||||
134
src/host/mod.rs
134
src/host/mod.rs
@@ -16,7 +16,6 @@ use filetime::{self, FileTime};
|
||||
use remotefs::fs::UnixPex;
|
||||
use remotefs::fs::{File, FileType, Metadata};
|
||||
use thiserror::Error;
|
||||
use wildmatch::WildMatch;
|
||||
|
||||
// Locals
|
||||
use crate::utils::path;
|
||||
@@ -112,7 +111,7 @@ impl Localhost {
|
||||
));
|
||||
}
|
||||
// Retrieve files for provided path
|
||||
host.files = match host.scan_dir(host.wrkdir.as_path()) {
|
||||
host.files = match host.list_dir(host.wrkdir.as_path()) {
|
||||
Ok(files) => files,
|
||||
Err(err) => {
|
||||
error!(
|
||||
@@ -131,12 +130,6 @@ impl Localhost {
|
||||
self.wrkdir.clone()
|
||||
}
|
||||
|
||||
/// List files in current directory
|
||||
#[allow(dead_code)]
|
||||
pub fn list_dir(&self) -> Vec<File> {
|
||||
self.files.clone()
|
||||
}
|
||||
|
||||
/// Change working directory with the new provided directory
|
||||
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
|
||||
let new_dir: PathBuf = self.to_path(new_dir);
|
||||
@@ -164,7 +157,7 @@ impl Localhost {
|
||||
// Change dir
|
||||
self.wrkdir = new_dir;
|
||||
// Scan new directory
|
||||
self.files = match self.scan_dir(self.wrkdir.as_path()) {
|
||||
self.files = match self.list_dir(self.wrkdir.as_path()) {
|
||||
Ok(files) => files,
|
||||
Err(err) => {
|
||||
error!("Could not scan new directory: {}", err);
|
||||
@@ -204,7 +197,7 @@ impl Localhost {
|
||||
Ok(_) => {
|
||||
// Update dir
|
||||
if dir_name.is_relative() {
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
}
|
||||
info!("Created directory {}", dir_path.display());
|
||||
Ok(())
|
||||
@@ -237,7 +230,7 @@ impl Localhost {
|
||||
match std::fs::remove_dir_all(entry.path()) {
|
||||
Ok(_) => {
|
||||
// Update dir
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
info!("Removed directory {}", entry.path().display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -265,7 +258,7 @@ impl Localhost {
|
||||
match std::fs::remove_file(entry.path()) {
|
||||
Ok(_) => {
|
||||
// Update dir
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
info!("Removed file {}", entry.path().display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -286,7 +279,7 @@ impl Localhost {
|
||||
match std::fs::rename(entry.path(), dst_path) {
|
||||
Ok(_) => {
|
||||
// Scan dir
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
debug!(
|
||||
"Moved file {} to {}",
|
||||
entry.path().display(),
|
||||
@@ -327,7 +320,7 @@ impl Localhost {
|
||||
self.mkdir(dst.as_path())?;
|
||||
}
|
||||
// Scan dir
|
||||
let dir_files: Vec<File> = self.scan_dir(entry.path())?;
|
||||
let dir_files: Vec<File> = self.list_dir(entry.path())?;
|
||||
// Iterate files
|
||||
for dir_entry in dir_files.iter() {
|
||||
// Calculate dst
|
||||
@@ -362,11 +355,11 @@ impl Localhost {
|
||||
match dst.is_dir() {
|
||||
true => {
|
||||
if dst == self.pwd().as_path() {
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
} else if let Some(parent) = dst.parent() {
|
||||
// If parent is pwd, scan directory
|
||||
if parent == self.pwd().as_path() {
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,7 +367,7 @@ impl Localhost {
|
||||
if let Some(parent) = dst.parent() {
|
||||
// If parent is pwd, scan directory
|
||||
if parent == self.pwd().as_path() {
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,7 +550,7 @@ impl Localhost {
|
||||
}
|
||||
|
||||
/// Get content of the current directory as a list of fs entry
|
||||
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<File>, HostError> {
|
||||
pub fn list_dir(&self, dir: &Path) -> Result<Vec<File>, HostError> {
|
||||
info!("Reading directory {}", dir.display());
|
||||
match std::fs::read_dir(dir) {
|
||||
Ok(e) => {
|
||||
@@ -579,12 +572,6 @@ impl Localhost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course.
|
||||
/// The `search` argument supports wilcards ('*', '?')
|
||||
pub fn find(&self, search: &str) -> Result<Vec<File>, HostError> {
|
||||
self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search))
|
||||
}
|
||||
|
||||
/// Create a symlink at path pointing at target
|
||||
#[cfg(unix)]
|
||||
pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> {
|
||||
@@ -600,41 +587,6 @@ impl Localhost {
|
||||
})
|
||||
}
|
||||
|
||||
// -- privates
|
||||
|
||||
/// Recursive call for `find` method.
|
||||
/// Search in current directory for files which match `filter`.
|
||||
/// If a directory is found in current directory, `iter_search` will be called using that dir as argument.
|
||||
fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result<Vec<File>, HostError> {
|
||||
// Scan directory
|
||||
let mut drained: Vec<File> = Vec::new();
|
||||
match self.scan_dir(dir) {
|
||||
Err(err) => Err(err),
|
||||
Ok(entries) => {
|
||||
// Iter entries
|
||||
/* For each entry:
|
||||
- if is dir: call iter_search with `dir`
|
||||
- push `iter_search` result to `drained`
|
||||
- if is file: check if it matches `filter`
|
||||
- if it matches `filter`: push to to filter
|
||||
*/
|
||||
for entry in entries.into_iter() {
|
||||
if entry.is_dir() {
|
||||
// If directory matches; push directory to drained
|
||||
let next_path = entry.path().to_path_buf();
|
||||
if filter.matches(entry.name().as_str()) {
|
||||
drained.push(entry);
|
||||
}
|
||||
drained.append(&mut self.iter_search(next_path.as_path(), filter)?);
|
||||
} else if filter.matches(entry.name().as_str()) {
|
||||
drained.push(entry);
|
||||
}
|
||||
}
|
||||
Ok(drained)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert path to absolute path
|
||||
fn to_path(&self, p: &Path) -> PathBuf {
|
||||
path::absolutize(self.wrkdir.as_path(), p)
|
||||
@@ -658,7 +610,7 @@ mod tests {
|
||||
use super::*;
|
||||
#[cfg(unix)]
|
||||
use crate::utils::test_helpers::make_fsentry;
|
||||
use crate::utils::test_helpers::{create_sample_file, make_dir_at, make_file_at};
|
||||
use crate::utils::test_helpers::{create_sample_file, make_file_at};
|
||||
|
||||
#[test]
|
||||
fn test_host_error_new() {
|
||||
@@ -711,19 +663,6 @@ mod tests {
|
||||
assert_eq!(host.pwd(), PathBuf::from("/dev"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_host_localhost_list_files() {
|
||||
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
// Scan dir
|
||||
let entries = std::fs::read_dir(PathBuf::from("/dev").as_path()).unwrap();
|
||||
let mut counter: usize = 0;
|
||||
for _ in entries {
|
||||
counter += 1;
|
||||
}
|
||||
assert_eq!(host.list_dir().len(), counter);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_host_localhost_change_dir() {
|
||||
@@ -813,7 +752,7 @@ mod tests {
|
||||
.is_ok());
|
||||
// Get dir
|
||||
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let files: Vec<File> = host.list_dir();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
// Verify files
|
||||
let file_0: &File = files.get(0).unwrap();
|
||||
if file_0.name() == *"foo.txt" {
|
||||
@@ -841,10 +780,10 @@ mod tests {
|
||||
fn test_host_localhost_mkdir() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let files: Vec<File> = host.list_dir();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 0); // There should be 0 files now
|
||||
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
|
||||
let files: Vec<File> = host.list_dir();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||
// Try to re-create directory
|
||||
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
|
||||
@@ -868,17 +807,17 @@ mod tests {
|
||||
// Create sample file
|
||||
assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let files: Vec<File> = host.list_dir();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||
// Remove file
|
||||
assert!(host.remove(files.get(0).unwrap()).is_ok());
|
||||
// There should be 0 files now
|
||||
let files: Vec<File> = host.list_dir();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 0); // There should be 0 files now
|
||||
// Create directory
|
||||
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
|
||||
// Delete directory
|
||||
let files: Vec<File> = host.list_dir();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||
assert!(host.remove(files.get(0).unwrap()).is_ok());
|
||||
// Remove unexisting directory
|
||||
@@ -899,7 +838,7 @@ mod tests {
|
||||
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str());
|
||||
assert!(StdFile::create(src_path.as_path()).is_ok());
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let files: Vec<File> = host.list_dir();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||
assert_eq!(files.get(0).unwrap().name(), "foo.txt");
|
||||
// Rename file
|
||||
@@ -909,7 +848,7 @@ mod tests {
|
||||
.rename(files.get(0).unwrap(), dst_path.as_path())
|
||||
.is_ok());
|
||||
// There should be still 1 file now, but named bar.txt
|
||||
let files: Vec<File> = host.list_dir();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 1); // There should be 0 files now
|
||||
assert_eq!(files.get(0).unwrap().name(), "bar.txt");
|
||||
// Fail
|
||||
@@ -1084,39 +1023,6 @@ mod tests {
|
||||
assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_host_find() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
let dir_path: &Path = tmpdir.path();
|
||||
// Make files
|
||||
assert!(make_file_at(dir_path, "pippo.txt").is_ok());
|
||||
assert!(make_file_at(dir_path, "foo.jpg").is_ok());
|
||||
// Make nested struct
|
||||
assert!(make_dir_at(dir_path, "examples").is_ok());
|
||||
let mut subdir: PathBuf = PathBuf::from(dir_path);
|
||||
subdir.push("examples/");
|
||||
assert!(make_file_at(subdir.as_path(), "omar.txt").is_ok());
|
||||
assert!(make_file_at(subdir.as_path(), "errors.txt").is_ok());
|
||||
assert!(make_file_at(subdir.as_path(), "screenshot.png").is_ok());
|
||||
assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok());
|
||||
let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
|
||||
// Find txt files
|
||||
let mut result: Vec<File> = host.find("*.txt").ok().unwrap();
|
||||
result.sort_by_key(|x: &File| x.name().to_lowercase());
|
||||
// There should be 3 entries
|
||||
assert_eq!(result.len(), 3);
|
||||
// Check names (they should be sorted alphabetically already; NOTE: examples/ comes before pippo.txt)
|
||||
assert_eq!(result[0].name(), "errors.txt");
|
||||
assert_eq!(result[1].name(), "omar.txt");
|
||||
assert_eq!(result[2].name(), "pippo.txt");
|
||||
// Search for directory
|
||||
let mut result: Vec<File> = host.find("examples*").ok().unwrap();
|
||||
result.sort_by_key(|x: &File| x.name().to_lowercase());
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].name(), "examples");
|
||||
assert_eq!(result[1].name(), "examples.csv");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn should_create_symlink() {
|
||||
|
||||
@@ -145,10 +145,13 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(all(
|
||||
any(target_os = "macos", target_os = "freebsd"),
|
||||
feature = "github-actions"
|
||||
)))]
|
||||
#[cfg(all(
|
||||
not(all(
|
||||
any(target_os = "macos", target_os = "freebsd"),
|
||||
feature = "github-actions"
|
||||
)),
|
||||
not(feature = "isolated-tests")
|
||||
))]
|
||||
fn auto_update() {
|
||||
// Wno version
|
||||
assert_eq!(
|
||||
@@ -162,10 +165,13 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(all(
|
||||
any(target_os = "macos", target_os = "freebsd"),
|
||||
feature = "github-actions"
|
||||
)))]
|
||||
#[cfg(all(
|
||||
not(all(
|
||||
any(target_os = "macos", target_os = "freebsd"),
|
||||
feature = "github-actions"
|
||||
)),
|
||||
not(feature = "isolated-tests")
|
||||
))]
|
||||
fn check_for_updates() {
|
||||
println!("{:?}", Update::is_new_version_available());
|
||||
assert!(Update::is_new_version_available().is_ok());
|
||||
|
||||
@@ -82,6 +82,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "isolated-tests"))]
|
||||
fn test_system_keys_keyringstorage() {
|
||||
let username: String = username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
@@ -89,17 +90,17 @@ mod tests {
|
||||
let app_name: &str = "termscp-test2";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
|
||||
let _ = kring.delete_password();
|
||||
let _ = kring.delete_credential();
|
||||
drop(kring);
|
||||
// Secret should not exist
|
||||
assert!(storage.get_key(app_name).is_err());
|
||||
// Write secret
|
||||
assert!(storage.set_key(app_name, secret).is_ok());
|
||||
// Get secret
|
||||
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
|
||||
assert_eq!(storage.get_key(app_name).unwrap().as_str(), secret);
|
||||
|
||||
// Delete the key manually...
|
||||
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
|
||||
assert!(kring.delete_password().is_ok());
|
||||
assert!(kring.delete_credential().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::time::Duration;
|
||||
|
||||
pub use change::FsChange;
|
||||
use notify::{
|
||||
watcher, DebouncedEvent, Error as WatcherError, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
Config, Error as WatcherError, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -27,6 +27,8 @@ pub enum FsWatcherError {
|
||||
PathNotWatched,
|
||||
#[error("unable to watch path, since it's already watched")]
|
||||
PathAlreadyWatched,
|
||||
#[error("unknown event: {0}")]
|
||||
UnknownEvent(&'static str),
|
||||
#[error("worker error: {0}")]
|
||||
WorkerError(WatcherError),
|
||||
}
|
||||
@@ -37,10 +39,61 @@ impl From<WatcherError> for FsWatcherError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes an event that can be received from the `FsWatcher`
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum FsWatcherEvent {
|
||||
Rename { source: PathBuf, dest: PathBuf },
|
||||
Remove(PathBuf),
|
||||
Create(PathBuf),
|
||||
Modify(PathBuf),
|
||||
Other,
|
||||
}
|
||||
|
||||
impl TryFrom<Event> for FsWatcherEvent {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(ev: Event) -> Result<Self, Self::Error> {
|
||||
match ev.kind {
|
||||
EventKind::Any | EventKind::Access(_) | EventKind::Other => Ok(Self::Other),
|
||||
EventKind::Create(_) => {
|
||||
if ev.paths.len() == 2 {
|
||||
Ok(Self::Rename {
|
||||
source: ev.paths[0].clone(),
|
||||
dest: ev.paths[1].clone(),
|
||||
})
|
||||
} else if let Some(p) = ev.paths.first() {
|
||||
Ok(Self::Create(p.clone()))
|
||||
} else {
|
||||
Err("No path found")
|
||||
}
|
||||
}
|
||||
EventKind::Modify(_) => {
|
||||
if ev.paths.len() == 2 {
|
||||
Ok(Self::Rename {
|
||||
source: ev.paths[0].clone(),
|
||||
dest: ev.paths[1].clone(),
|
||||
})
|
||||
} else if let Some(p) = ev.paths.first() {
|
||||
Ok(Self::Modify(p.clone()))
|
||||
} else {
|
||||
Err("No path found")
|
||||
}
|
||||
}
|
||||
EventKind::Remove(_) => {
|
||||
if let Some(p) = ev.paths.first() {
|
||||
Ok(Self::Remove(p.clone()))
|
||||
} else {
|
||||
Err("No path found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// File system watcher
|
||||
pub struct FsWatcher {
|
||||
paths: HashMap<PathBuf, PathBuf>,
|
||||
receiver: Receiver<DebouncedEvent>,
|
||||
receiver: Receiver<notify::Result<Event>>,
|
||||
watcher: RecommendedWatcher,
|
||||
}
|
||||
|
||||
@@ -52,29 +105,32 @@ impl FsWatcher {
|
||||
Ok(Self {
|
||||
paths: HashMap::default(),
|
||||
receiver,
|
||||
watcher: watcher(tx, delay)?,
|
||||
watcher: RecommendedWatcher::new(tx, Config::default().with_poll_interval(delay))?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Poll searching for the first available disk change
|
||||
pub fn poll(&self) -> FsWatcherResult<Option<FsChange>> {
|
||||
match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
||||
Ok(DebouncedEvent::Rename(source, dest)) => Ok(self.build_fs_move(source, dest)),
|
||||
Ok(DebouncedEvent::Remove(p)) => Ok(self.build_fs_remove(p)),
|
||||
Ok(DebouncedEvent::Chmod(p) | DebouncedEvent::Create(p) | DebouncedEvent::Write(p)) => {
|
||||
Ok(self.build_fs_update(p))
|
||||
}
|
||||
Ok(
|
||||
DebouncedEvent::Rescan
|
||||
| DebouncedEvent::NoticeRemove(_)
|
||||
| DebouncedEvent::NoticeWrite(_),
|
||||
) => Ok(None),
|
||||
Ok(DebouncedEvent::Error(e, _)) => {
|
||||
error!("FsWatcher reported error: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => Ok(None),
|
||||
let res = match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
||||
Ok(res) => res,
|
||||
Err(RecvTimeoutError::Timeout) => return Ok(None),
|
||||
Err(RecvTimeoutError::Disconnected) => panic!("File watcher died"),
|
||||
};
|
||||
|
||||
// convert event to FsChange
|
||||
let event = res
|
||||
.map(FsWatcherEvent::try_from)
|
||||
.map_err(FsWatcherError::from)?
|
||||
.map_err(FsWatcherError::UnknownEvent)?;
|
||||
|
||||
match event {
|
||||
FsWatcherEvent::Rename { source, dest } => Ok(self.build_fs_move(source, dest)),
|
||||
FsWatcherEvent::Remove(p) => Ok(self.build_fs_remove(p)),
|
||||
FsWatcherEvent::Modify(p) | FsWatcherEvent::Create(p) => Ok(self.build_fs_update(p)),
|
||||
FsWatcherEvent::Other => {
|
||||
debug!("unknown event");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -193,8 +193,6 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
fn load_bookmark_kube_into_gui(&mut self, params: KubeProtocolParams) {
|
||||
self.mount_kube_pod_name(params.pod.as_str());
|
||||
self.mount_kube_container(¶ms.container);
|
||||
self.mount_kube_cluster_url(params.cluster_url.as_deref().unwrap_or(""));
|
||||
self.mount_kube_namespace(params.namespace.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_cert(params.client_cert.as_deref().unwrap_or(""));
|
||||
|
||||
@@ -832,40 +832,6 @@ impl Component<Msg, NoUserEvent> for InputWebDAVUri {
|
||||
|
||||
// kube
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputKubePodName {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl InputKubePodName {
|
||||
pub fn new(bucket: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder("pod-name", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title("Pod name", Alignment::Left)
|
||||
.input_type(InputType::Text)
|
||||
.value(bucket),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InputKubePodName {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_input_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Ui(UiMsg::KubePodNameBlurDown),
|
||||
Msg::Ui(UiMsg::KubePodNameBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputKubeNamespace {
|
||||
component: Input,
|
||||
@@ -937,40 +903,6 @@ impl Component<Msg, NoUserEvent> for InputKubeClusterUrl {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputKubeContainer {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl InputKubeContainer {
|
||||
pub fn new(bucket: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder("container", Style::default().fg(Color::Rgb(128, 128, 128)))
|
||||
.title("Kube container", Alignment::Left)
|
||||
.input_type(InputType::Text)
|
||||
.value(bucket),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InputKubeContainer {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
handle_input_ev(
|
||||
self,
|
||||
ev,
|
||||
Msg::Ui(UiMsg::KubeContainerBlurDown),
|
||||
Msg::Ui(UiMsg::KubeContainerBlurUp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputKubeUsername {
|
||||
component: Input,
|
||||
|
||||
@@ -16,12 +16,11 @@ pub use bookmarks::{
|
||||
#[cfg(unix)]
|
||||
pub use form::InputSmbWorkgroup;
|
||||
pub use form::{
|
||||
InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeContainer,
|
||||
InputKubeNamespace, InputKubePodName, InputKubeUsername, InputLocalDirectory, InputPassword,
|
||||
InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint,
|
||||
InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
|
||||
InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, ProtocolRadio,
|
||||
RadioS3NewPathStyle,
|
||||
InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeNamespace,
|
||||
InputKubeUsername, InputLocalDirectory, InputPassword, InputPort, InputRemoteDirectory,
|
||||
InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile, InputS3Region,
|
||||
InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputSmbShare,
|
||||
InputUsername, InputWebDAVUri, ProtocolRadio, RadioS3NewPathStyle,
|
||||
};
|
||||
pub use popup::{
|
||||
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
||||
|
||||
@@ -85,9 +85,7 @@ impl AuthActivity {
|
||||
/// Get input values from fields or return an error if fields are invalid to work as aws s3
|
||||
pub(super) fn collect_kube_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_kube_params_input();
|
||||
if params.pod.is_empty() {
|
||||
return Err("Invalid pod name");
|
||||
}
|
||||
|
||||
Ok(FileTransferParams {
|
||||
protocol: FileTransferProtocol::Kube,
|
||||
params: ProtocolParams::Kube(params),
|
||||
|
||||
@@ -48,8 +48,6 @@ pub enum Id {
|
||||
InfoPopup,
|
||||
InstallUpdatePopup,
|
||||
Keybindings,
|
||||
KubePodName,
|
||||
KubeContainer,
|
||||
KubeNamespace,
|
||||
KubeClusterUrl,
|
||||
KubeUsername,
|
||||
@@ -119,10 +117,6 @@ pub enum UiMsg {
|
||||
CloseKeybindingsPopup,
|
||||
CloseQuitPopup,
|
||||
CloseSaveBookmark,
|
||||
KubePodNameBlurDown,
|
||||
KubePodNameBlurUp,
|
||||
KubeContainerBlurDown,
|
||||
KubeContainerBlurUp,
|
||||
KubeNamespaceBlurDown,
|
||||
KubeNamespaceBlurUp,
|
||||
KubeClusterUrlBlurDown,
|
||||
|
||||
@@ -70,7 +70,7 @@ impl AuthActivity {
|
||||
InputMask::Generic => &Id::Password,
|
||||
InputMask::Smb => &Id::Password,
|
||||
InputMask::AwsS3 => &Id::S3Bucket,
|
||||
InputMask::Kube => &Id::KubePodName,
|
||||
InputMask::Kube => &Id::KubeNamespace,
|
||||
InputMask::WebDAV => &Id::Password,
|
||||
})
|
||||
.is_ok());
|
||||
@@ -84,7 +84,7 @@ impl AuthActivity {
|
||||
InputMask::Generic => &Id::Password,
|
||||
InputMask::Smb => &Id::Password,
|
||||
InputMask::AwsS3 => &Id::S3Bucket,
|
||||
InputMask::Kube => &Id::KubePodName,
|
||||
InputMask::Kube => &Id::KubeNamespace,
|
||||
InputMask::WebDAV => &Id::Password,
|
||||
})
|
||||
.is_ok());
|
||||
@@ -210,7 +210,7 @@ impl AuthActivity {
|
||||
InputMask::Generic => &Id::Address,
|
||||
InputMask::Smb => &Id::Address,
|
||||
InputMask::AwsS3 => &Id::S3Bucket,
|
||||
InputMask::Kube => &Id::KubePodName,
|
||||
InputMask::Kube => &Id::KubeNamespace,
|
||||
InputMask::WebDAV => &Id::WebDAVUri,
|
||||
})
|
||||
.is_ok());
|
||||
@@ -305,23 +305,11 @@ impl AuthActivity {
|
||||
UiMsg::KubeClientKeyBlurUp => {
|
||||
assert!(self.app.active(&Id::KubeClientCert).is_ok());
|
||||
}
|
||||
UiMsg::KubeContainerBlurDown => {
|
||||
assert!(self.app.active(&Id::KubeNamespace).is_ok());
|
||||
}
|
||||
UiMsg::KubeContainerBlurUp => {
|
||||
assert!(self.app.active(&Id::KubePodName).is_ok());
|
||||
}
|
||||
UiMsg::KubePodNameBlurDown => {
|
||||
assert!(self.app.active(&Id::KubeContainer).is_ok());
|
||||
}
|
||||
UiMsg::KubePodNameBlurUp => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
}
|
||||
UiMsg::KubeNamespaceBlurDown => {
|
||||
assert!(self.app.active(&Id::KubeClusterUrl).is_ok());
|
||||
}
|
||||
UiMsg::KubeNamespaceBlurUp => {
|
||||
assert!(self.app.active(&Id::KubeContainer).is_ok());
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
}
|
||||
UiMsg::KubeClusterUrlBlurDown => {
|
||||
assert!(self.app.active(&Id::KubeUsername).is_ok());
|
||||
|
||||
@@ -64,9 +64,7 @@ impl AuthActivity {
|
||||
self.mount_kube_client_cert("");
|
||||
self.mount_kube_client_key("");
|
||||
self.mount_kube_cluster_url("");
|
||||
self.mount_kube_container("");
|
||||
self.mount_kube_namespace("");
|
||||
self.mount_kube_pod_name("");
|
||||
self.mount_kube_username("");
|
||||
self.mount_smb_share("");
|
||||
#[cfg(unix)]
|
||||
@@ -149,86 +147,18 @@ impl AuthActivity {
|
||||
.direction(Direction::Vertical)
|
||||
.split(main_chunks[0]);
|
||||
// Input mask chunks
|
||||
let input_mask = match self.input_mask() {
|
||||
InputMask::AwsS3 => Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // bucket
|
||||
Constraint::Length(3), // region
|
||||
Constraint::Length(3), // profile
|
||||
Constraint::Length(3), // access_key
|
||||
Constraint::Length(3), // remote directory
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(auth_chunks[4]),
|
||||
InputMask::Kube => Layout::default()
|
||||
.constraints([
|
||||
Constraint::Length(3), // ...
|
||||
Constraint::Length(3), // ...
|
||||
Constraint::Length(3), // ...
|
||||
Constraint::Length(3), // ...
|
||||
Constraint::Length(3), // remote directory
|
||||
])
|
||||
.direction(Direction::Vertical)
|
||||
.split(auth_chunks[4]),
|
||||
InputMask::Generic => Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // address
|
||||
Constraint::Length(3), // port
|
||||
Constraint::Length(3), // username
|
||||
Constraint::Length(3), // password
|
||||
Constraint::Length(3), // remote directory
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(auth_chunks[4]),
|
||||
#[cfg(unix)]
|
||||
InputMask::Smb => Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // address
|
||||
Constraint::Length(3), // port
|
||||
Constraint::Length(3), // share
|
||||
Constraint::Length(3), // username
|
||||
Constraint::Length(3), // password
|
||||
Constraint::Length(3), // workgroup
|
||||
Constraint::Length(3), // remote directory
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(auth_chunks[4]),
|
||||
#[cfg(windows)]
|
||||
InputMask::Smb => Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // address
|
||||
Constraint::Length(3), // share
|
||||
Constraint::Length(3), // username
|
||||
Constraint::Length(3), // password
|
||||
Constraint::Length(3), // remote directory
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(auth_chunks[4]),
|
||||
InputMask::WebDAV => Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // uri
|
||||
Constraint::Length(3), // username
|
||||
Constraint::Length(3), // password
|
||||
Constraint::Length(3), // dir
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(auth_chunks[4]),
|
||||
};
|
||||
let input_mask = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // uri
|
||||
Constraint::Length(3), // username
|
||||
Constraint::Length(3), // password
|
||||
Constraint::Length(3), // dir
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(auth_chunks[4]);
|
||||
// Create bookmark chunks
|
||||
let bookmark_chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
@@ -347,7 +277,7 @@ impl AuthActivity {
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Input form
|
||||
Constraint::Length(2), // Yes/No
|
||||
Constraint::Length(4), // Yes/No
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
@@ -816,30 +746,6 @@ impl AuthActivity {
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn mount_kube_pod_name(&mut self, value: &str) {
|
||||
let color = self.theme().auth_address;
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::KubePodName,
|
||||
Box::new(components::InputKubePodName::new(value, color)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn mount_kube_container(&mut self, value: &str) {
|
||||
let color = self.theme().auth_password;
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::KubeContainer,
|
||||
Box::new(components::InputKubeContainer::new(value, color)),
|
||||
vec![]
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn mount_kube_namespace(&mut self, value: &str) {
|
||||
let color = self.theme().auth_port;
|
||||
assert!(self
|
||||
@@ -974,16 +880,12 @@ impl AuthActivity {
|
||||
|
||||
/// Collect s3 input values from view
|
||||
pub(super) fn get_kube_params_input(&self) -> KubeProtocolParams {
|
||||
let pod = self.get_input_kube_pod_name();
|
||||
let container = self.get_input_kube_container();
|
||||
let namespace = self.get_input_kube_namespace();
|
||||
let cluster_url = self.get_input_kube_cluster_url();
|
||||
let username = self.get_input_kube_username();
|
||||
let client_cert = self.get_input_kube_client_cert();
|
||||
let client_key = self.get_input_kube_client_key();
|
||||
KubeProtocolParams {
|
||||
pod,
|
||||
container,
|
||||
namespace,
|
||||
cluster_url,
|
||||
username,
|
||||
@@ -1069,10 +971,7 @@ impl AuthActivity {
|
||||
|
||||
pub(super) fn get_input_port(&self) -> u16 {
|
||||
match self.app.state(&Id::Port) {
|
||||
Ok(State::One(StateValue::String(x))) => match u16::from_str(x.as_str()) {
|
||||
Ok(v) => v,
|
||||
_ => 0,
|
||||
},
|
||||
Ok(State::One(StateValue::String(x))) => u16::from_str(x.as_str()).unwrap_or_default(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@@ -1154,20 +1053,6 @@ impl AuthActivity {
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn get_input_kube_pod_name(&self) -> String {
|
||||
match self.app.state(&Id::KubePodName) {
|
||||
Ok(State::One(StateValue::String(x))) => x,
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_input_kube_container(&self) -> String {
|
||||
match self.app.state(&Id::KubeContainer) {
|
||||
Ok(State::One(StateValue::String(x))) => x,
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_input_kube_namespace(&self) -> Option<String> {
|
||||
match self.app.state(&Id::KubeNamespace) {
|
||||
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||
@@ -1285,15 +1170,13 @@ impl AuthActivity {
|
||||
}
|
||||
ProtocolParams::Kube(params) => {
|
||||
format!(
|
||||
"{}://{}@{}{}{}",
|
||||
"{}://{}{}",
|
||||
protocol,
|
||||
params.container,
|
||||
params.pod,
|
||||
params
|
||||
.namespace
|
||||
.as_deref()
|
||||
.map(|x| format!("/{x}"))
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or_else(|| String::from("default")),
|
||||
params
|
||||
.cluster_url
|
||||
.as_deref()
|
||||
@@ -1389,18 +1272,6 @@ impl AuthActivity {
|
||||
/// Get the visible element in the kube form, based on current focus
|
||||
fn get_kube_view(&self) -> [Id; 4] {
|
||||
match self.app.focus() {
|
||||
Some(&Id::KubePodName) => [
|
||||
Id::KubePodName,
|
||||
Id::KubeContainer,
|
||||
Id::KubeNamespace,
|
||||
Id::KubeClusterUrl,
|
||||
],
|
||||
Some(&Id::KubeUsername) => [
|
||||
Id::KubeContainer,
|
||||
Id::KubeNamespace,
|
||||
Id::KubeClusterUrl,
|
||||
Id::KubeUsername,
|
||||
],
|
||||
Some(&Id::KubeClientCert) => [
|
||||
Id::KubeNamespace,
|
||||
Id::KubeClusterUrl,
|
||||
@@ -1426,10 +1297,10 @@ impl AuthActivity {
|
||||
Id::LocalDirectory,
|
||||
],
|
||||
_ => [
|
||||
Id::KubePodName,
|
||||
Id::KubeContainer,
|
||||
Id::KubeNamespace,
|
||||
Id::KubeClusterUrl,
|
||||
Id::KubeUsername,
|
||||
Id::KubeClientCert,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,23 +9,10 @@ use super::super::browser::FileExplorerTab;
|
||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<File>, String> {
|
||||
match self.host.find(input.as_str()) {
|
||||
Ok(entries) => Ok(entries),
|
||||
Err(err) => Err(format!("Could not search for files: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<File>, String> {
|
||||
match self.client.as_mut().find(input.as_str()) {
|
||||
Ok(entries) => Ok(entries),
|
||||
Err(err) => Err(format!("Could not search for files: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_find_changedir(&mut self) {
|
||||
// Match entry
|
||||
if let SelectedFile::One(entry) = self.get_found_selected_entries() {
|
||||
debug!("Changedir to: {}", entry.name());
|
||||
// Get path: if a directory, use directory path; if it is a File, get parent path
|
||||
let path = if entry.is_dir() {
|
||||
entry.path().to_path_buf()
|
||||
|
||||
@@ -27,8 +27,10 @@ pub(crate) mod open;
|
||||
mod pending;
|
||||
pub(crate) mod rename;
|
||||
pub(crate) mod save;
|
||||
pub(crate) mod scan;
|
||||
pub(crate) mod submit;
|
||||
pub(crate) mod symlink;
|
||||
pub(crate) mod walkdir;
|
||||
pub(crate) mod watcher;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
19
src/ui/activities/filetransfer/actions/scan.rs
Normal file
19
src/ui/activities/filetransfer/actions/scan.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use std::path::Path;
|
||||
|
||||
use super::{File, FileTransferActivity};
|
||||
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_scan(&mut self, p: &Path) -> Result<Vec<File>, String> {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => self
|
||||
.host
|
||||
.list_dir(p)
|
||||
.map_err(|e| format!("Failed to list directory: {}", e)),
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self
|
||||
.client
|
||||
.list_dir(p)
|
||||
.map_err(|e| format!("Failed to list directory: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/ui/activities/filetransfer/actions/walkdir.rs
Normal file
97
src/ui/activities/filetransfer/actions/walkdir.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{File, FileTransferActivity};
|
||||
use crate::ui::activities::filetransfer::lib::walkdir::WalkdirStates;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WalkdirError {
|
||||
Aborted,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_walkdir_local(&mut self) -> Result<Vec<File>, WalkdirError> {
|
||||
let mut acc = Vec::with_capacity(32_768);
|
||||
|
||||
self.walkdir(&mut acc, &self.host.pwd(), |activity, path| {
|
||||
activity.host.list_dir(path).map_err(|e| e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
pub(crate) fn action_walkdir_remote(&mut self) -> Result<Vec<File>, WalkdirError> {
|
||||
let mut acc = Vec::with_capacity(32_768);
|
||||
|
||||
let pwd = self
|
||||
.client
|
||||
.pwd()
|
||||
.map_err(|e| WalkdirError::Error(e.to_string()))?;
|
||||
|
||||
self.walkdir(&mut acc, &pwd, |activity, path| {
|
||||
activity.client.list_dir(path).map_err(|e| e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
fn walkdir<F>(
|
||||
&mut self,
|
||||
acc: &mut Vec<File>,
|
||||
path: &Path,
|
||||
list_dir_fn: F,
|
||||
) -> Result<(), WalkdirError>
|
||||
where
|
||||
F: Fn(&mut Self, &Path) -> Result<Vec<File>, String> + Copy,
|
||||
{
|
||||
// init acc if empty
|
||||
if acc.is_empty() {
|
||||
self.init_walkdir();
|
||||
}
|
||||
|
||||
// list current directory
|
||||
let dir_entries = list_dir_fn(self, path).map_err(WalkdirError::Error)?;
|
||||
|
||||
// get dirs to scan later
|
||||
let dirs = dir_entries
|
||||
.iter()
|
||||
.filter(|entry| entry.is_dir())
|
||||
.map(|entry| entry.path.clone())
|
||||
.collect::<Vec<PathBuf>>();
|
||||
|
||||
// extend acc
|
||||
acc.extend(dir_entries.clone());
|
||||
// update view
|
||||
self.update_walkdir_entries(acc.len());
|
||||
|
||||
// check aborted
|
||||
self.check_aborted()?;
|
||||
|
||||
for dir in dirs {
|
||||
self.walkdir(acc, &dir, list_dir_fn)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_aborted(&mut self) -> Result<(), WalkdirError> {
|
||||
// read events
|
||||
self.tick();
|
||||
|
||||
// check if the user wants to abort
|
||||
if self.walkdir.aborted {
|
||||
return Err(WalkdirError::Aborted);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_walkdir(&mut self) {
|
||||
self.walkdir = WalkdirStates::default();
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent};
|
||||
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table};
|
||||
use tuirealm::tui::layout::Corner;
|
||||
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState};
|
||||
use tuirealm::tui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue};
|
||||
|
||||
use super::{Msg, UiMsg};
|
||||
@@ -63,7 +62,7 @@ impl MockComponent for Log {
|
||||
focus,
|
||||
None,
|
||||
))
|
||||
.start_corner(Corner::BottomLeft)
|
||||
.direction(ListDirection::BottomToTop)
|
||||
.highlight_symbol(">> ")
|
||||
.style(Style::default().bg(bg))
|
||||
.highlight_style(Style::default());
|
||||
|
||||
@@ -17,12 +17,13 @@ mod transfer;
|
||||
pub use misc::FooterBar;
|
||||
pub use popups::{
|
||||
ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup,
|
||||
FileInfoPopup, FilterPopup, FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
||||
FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
||||
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
||||
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
|
||||
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WatchedPathsList, WatcherPopup,
|
||||
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList,
|
||||
WatcherPopup, ATTR_FILES,
|
||||
};
|
||||
pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote};
|
||||
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
|
||||
|
||||
pub use self::log::Log;
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
//!
|
||||
//! popups components
|
||||
|
||||
mod chmod;
|
||||
mod goto;
|
||||
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use bytesize::ByteSize;
|
||||
@@ -16,15 +19,13 @@ use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
#[cfg(unix)]
|
||||
use users::{get_group_by_gid, get_user_by_uid};
|
||||
|
||||
pub use self::chmod::ChmodPopup;
|
||||
pub use self::goto::{GotoPopup, ATTR_FILES};
|
||||
use super::super::Browser;
|
||||
use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
|
||||
use crate::explorer::FileSorting;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
|
||||
mod chmod;
|
||||
|
||||
pub use chmod::ChmodPopup;
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct CopyPopup {
|
||||
component: Input,
|
||||
@@ -131,7 +132,10 @@ impl FilterPopup {
|
||||
"regex or wildmatch",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
)
|
||||
.title("Filter files by regex or wildmatch", Alignment::Center),
|
||||
.title(
|
||||
"Filter files by regex or wildmatch in the current directory",
|
||||
Alignment::Center,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -580,176 +584,6 @@ impl Component<Msg, NoUserEvent> for FileInfoPopup {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct FindPopup {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl FindPopup {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.input_type(InputType::Text)
|
||||
.placeholder(
|
||||
"Search files by name",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
)
|
||||
.title("*.txt", Alignment::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for FindPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.state() {
|
||||
State::One(StateValue::String(i)) => {
|
||||
Some(Msg::Transfer(TransferMsg::SearchFile(i)))
|
||||
}
|
||||
_ => Some(Msg::None),
|
||||
},
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseFindPopup))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct GoToPopup {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl GoToPopup {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.input_type(InputType::Text)
|
||||
.placeholder(
|
||||
"/foo/bar/buzz",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
)
|
||||
.title("Go to…", Alignment::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for GoToPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.state() {
|
||||
State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::GoTo(i))),
|
||||
_ => Some(Msg::None),
|
||||
},
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseGotoPopup))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct KeybindingsPopup {
|
||||
component: List,
|
||||
@@ -1675,6 +1509,7 @@ impl SortingPopup {
|
||||
FileSorting::ModifyTime => 1,
|
||||
FileSorting::Name => 0,
|
||||
FileSorting::Size => 3,
|
||||
FileSorting::None => 0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1778,6 +1613,7 @@ fn file_sorting_label(sorting: FileSorting) -> &'static str {
|
||||
FileSorting::ModifyTime => "By modify time",
|
||||
FileSorting::Name => "By name",
|
||||
FileSorting::Size => "By size",
|
||||
FileSorting::None => "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1978,6 +1814,47 @@ impl Component<Msg, NoUserEvent> for WaitPopup {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct WalkdirWaitPopup {
|
||||
component: Paragraph,
|
||||
}
|
||||
|
||||
impl WalkdirWaitPopup {
|
||||
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||
Self {
|
||||
component: Paragraph::default()
|
||||
.alignment(Alignment::Center)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[
|
||||
TextSpan::from(text.as_ref()),
|
||||
TextSpan::from("Press 'CTRL+C' to abort"),
|
||||
])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for WalkdirWaitPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
if matches!(
|
||||
ev,
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL
|
||||
})
|
||||
) {
|
||||
Some(Msg::Transfer(TransferMsg::AbortWalkdir))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct WatchedPathsList {
|
||||
component: List,
|
||||
|
||||
435
src/ui/activities/filetransfer/components/popups/goto.rs
Normal file
435
src/ui/activities/filetransfer/components/popups/goto.rs
Normal file
@@ -0,0 +1,435 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tui_realm_stdlib::Input;
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent};
|
||||
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||
use tuirealm::{
|
||||
AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent, State, StateValue,
|
||||
};
|
||||
|
||||
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
|
||||
|
||||
pub const ATTR_FILES: &str = "files";
|
||||
|
||||
#[derive(Default)]
|
||||
struct OwnStates {
|
||||
/// Path and name of the files
|
||||
files: Vec<(String, String)>,
|
||||
search: Option<String>,
|
||||
last_suggestion: Option<String>,
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
pub fn set_files(&mut self, files: Vec<String>) {
|
||||
self.files = files
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
(
|
||||
f.clone(),
|
||||
PathBuf::from(&f)
|
||||
.file_name()
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or(f),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
enum Suggestion {
|
||||
/// No suggestion
|
||||
None,
|
||||
/// Suggest a string
|
||||
Suggest(String),
|
||||
/// Rescan at `path` is required to satisfy the user input
|
||||
Rescan(PathBuf),
|
||||
}
|
||||
|
||||
impl From<CmdResult> for Suggestion {
|
||||
fn from(value: CmdResult) -> Self {
|
||||
match value {
|
||||
CmdResult::Batch(v) if v.len() == 1 => {
|
||||
if let CmdResult::Submit(State::One(StateValue::String(s))) = v.first().unwrap() {
|
||||
Suggestion::Suggest(s.clone())
|
||||
} else {
|
||||
Suggestion::None
|
||||
}
|
||||
}
|
||||
CmdResult::Batch(v) if v.len() == 2 => {
|
||||
if let CmdResult::Submit(State::One(StateValue::String(s))) = v.get(1).unwrap() {
|
||||
Suggestion::Rescan(PathBuf::from(s))
|
||||
} else {
|
||||
Suggestion::None
|
||||
}
|
||||
}
|
||||
_ => Suggestion::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Suggestion> for CmdResult {
|
||||
fn from(value: Suggestion) -> Self {
|
||||
match value {
|
||||
Suggestion::None => CmdResult::None,
|
||||
Suggestion::Suggest(s) => {
|
||||
CmdResult::Batch(vec![CmdResult::Submit(State::One(StateValue::String(s)))])
|
||||
}
|
||||
Suggestion::Rescan(p) => CmdResult::Batch(vec![
|
||||
CmdResult::None,
|
||||
CmdResult::Submit(State::One(StateValue::String(
|
||||
p.to_string_lossy().to_string(),
|
||||
))),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
/// Return the current suggestion if any, otherwise return search
|
||||
pub fn computed_search(&self) -> String {
|
||||
match (&self.search, &self.last_suggestion) {
|
||||
(_, Some(s)) => s.clone(),
|
||||
(Some(s), _) => s.clone(),
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Suggest files based on the input
|
||||
pub fn suggest(&mut self, input: &str) -> Suggestion {
|
||||
debug!(
|
||||
"Suggesting for: {input}; files {files:?}",
|
||||
files = self.files
|
||||
);
|
||||
|
||||
let is_path = PathBuf::from(input).is_absolute();
|
||||
|
||||
// case 1. search if any file starts with the input; get first if suggestion is `None`, otherwise get first after suggestion
|
||||
let suggestions: Vec<&String> = self
|
||||
.files
|
||||
.iter()
|
||||
.filter(|(path, file_name)| {
|
||||
if is_path {
|
||||
path.contains(input)
|
||||
} else {
|
||||
file_name.contains(input)
|
||||
}
|
||||
})
|
||||
.map(|(path, _)| path)
|
||||
.collect();
|
||||
|
||||
debug!("Suggestions for {input}: {:?}", suggestions);
|
||||
|
||||
// case 1. if suggestions not empty; then suggest next
|
||||
if !suggestions.is_empty() {
|
||||
let suggestion;
|
||||
if let Some(last_suggestion) = self.last_suggestion.take() {
|
||||
suggestion = suggestions
|
||||
.iter()
|
||||
.skip_while(|f| **f != &last_suggestion)
|
||||
.nth(1)
|
||||
.unwrap_or_else(|| suggestions.first().unwrap())
|
||||
.to_string();
|
||||
} else {
|
||||
suggestion = suggestions.first().map(|x| x.to_string()).unwrap();
|
||||
}
|
||||
|
||||
debug!("Suggested: {suggestion}");
|
||||
self.last_suggestion = Some(suggestion.clone());
|
||||
|
||||
return Suggestion::Suggest(suggestion);
|
||||
}
|
||||
|
||||
self.last_suggestion = None;
|
||||
|
||||
// case 2. otherwise convert suggest to a path and get the parent
|
||||
// to rescan the files
|
||||
let input_as_path = if input.starts_with('/') {
|
||||
input.to_string()
|
||||
} else {
|
||||
format!("./{}", input)
|
||||
};
|
||||
|
||||
let p = PathBuf::from(input_as_path);
|
||||
let parent = p
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| PathBuf::from("/"));
|
||||
|
||||
// if path is `.`, then return None
|
||||
if parent == PathBuf::from(".") {
|
||||
return Suggestion::None;
|
||||
}
|
||||
|
||||
debug!("Rescan required at: {}", parent.display());
|
||||
|
||||
Suggestion::Rescan(parent)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GotoPopup {
|
||||
input: Input,
|
||||
states: OwnStates,
|
||||
}
|
||||
|
||||
impl GotoPopup {
|
||||
pub fn new(color: Color, files: Vec<String>) -> Self {
|
||||
let mut states = OwnStates::default();
|
||||
states.set_files(files);
|
||||
|
||||
Self {
|
||||
input: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.input_type(InputType::Text)
|
||||
.placeholder(
|
||||
"/foo/bar/buzz",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
)
|
||||
.title("Go to… (Press <TAB> for autocompletion)", Alignment::Center),
|
||||
states,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MockComponent for GotoPopup {
|
||||
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::prelude::Rect) {
|
||||
self.input.view(frame, area);
|
||||
}
|
||||
|
||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||
match attr {
|
||||
Attribute::Custom(ATTR_FILES) => {
|
||||
let files = value
|
||||
.unwrap_payload()
|
||||
.unwrap_vec()
|
||||
.into_iter()
|
||||
.map(|x| x.unwrap_str())
|
||||
.collect();
|
||||
|
||||
self.states.set_files(files);
|
||||
// call perform Change
|
||||
self.perform(Cmd::Change);
|
||||
}
|
||||
_ => self.input.attr(attr, value),
|
||||
}
|
||||
}
|
||||
|
||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||
self.input.query(attr)
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::One(StateValue::String(self.states.computed_search()))
|
||||
}
|
||||
|
||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||
match cmd {
|
||||
Cmd::Change => {
|
||||
let input = self
|
||||
.states
|
||||
.search
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.input.state().unwrap_one().unwrap_string());
|
||||
let suggest = self.states.suggest(&input);
|
||||
if let Suggestion::Suggest(suggestion) = suggest.clone() {
|
||||
self.input
|
||||
.attr(Attribute::Value, AttrValue::String(suggestion.clone()));
|
||||
}
|
||||
|
||||
suggest.into()
|
||||
}
|
||||
cmd => {
|
||||
let res = self.input.perform(cmd);
|
||||
if let CmdResult::Changed(State::One(StateValue::String(new_text))) = &res {
|
||||
self.states.search = Some(new_text.clone());
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for GotoPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
if let Suggestion::Rescan(path) = Suggestion::from(self.perform(Cmd::Change)) {
|
||||
Some(Msg::Transfer(TransferMsg::RescanGotoFiles(path)))
|
||||
} else {
|
||||
Some(Msg::None)
|
||||
}
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.state() {
|
||||
State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::GoTo(i))),
|
||||
_ => Some(Msg::None),
|
||||
},
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseGotoPopup))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_should_convert_from_and_back_cmd_result() {
|
||||
let s = Suggestion::Suggest("foo".to_string());
|
||||
let cmd: CmdResult = s.clone().into();
|
||||
let s2: Suggestion = cmd.into();
|
||||
assert_eq!(s, s2);
|
||||
|
||||
let s = Suggestion::Rescan(PathBuf::from("/foo/bar"));
|
||||
let cmd: CmdResult = s.clone().into();
|
||||
let s2: Suggestion = cmd.into();
|
||||
assert_eq!(s, s2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_suggest_next() {
|
||||
let mut states = OwnStates {
|
||||
files: vec![
|
||||
("/home/foo".to_string(), "foo".to_string()),
|
||||
("/home/bar".to_string(), "bar".to_string()),
|
||||
("/home/buzz".to_string(), "buzz".to_string()),
|
||||
("/home/fizz".to_string(), "fizz".to_string()),
|
||||
],
|
||||
search: None,
|
||||
last_suggestion: None,
|
||||
};
|
||||
|
||||
let s = states.suggest("f");
|
||||
assert_eq!(Suggestion::Suggest("/home/foo".to_string()), s);
|
||||
let s = states.suggest("f");
|
||||
assert_eq!(Suggestion::Suggest("/home/fizz".to_string()), s);
|
||||
|
||||
let s = states.suggest("f");
|
||||
assert_eq!(Suggestion::Suggest("/home/foo".to_string()), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_should_suggest_absolute_path() {
|
||||
let mut states = OwnStates {
|
||||
files: vec![
|
||||
("/home/foo".to_string(), "foo".to_string()),
|
||||
("/home/bar".to_string(), "bar".to_string()),
|
||||
("/home/buzz".to_string(), "buzz".to_string()),
|
||||
("/home/fizz".to_string(), "fizz".to_string()),
|
||||
],
|
||||
search: None,
|
||||
last_suggestion: None,
|
||||
};
|
||||
|
||||
let s = states.suggest("/home/f");
|
||||
assert_eq!(Suggestion::Suggest("/home/foo".to_string()), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_suggest_rescan() {
|
||||
let mut states = OwnStates {
|
||||
files: vec![
|
||||
("/home/foo".to_string(), "foo".to_string()),
|
||||
("/home/bar".to_string(), "bar".to_string()),
|
||||
("/home/buzz".to_string(), "buzz".to_string()),
|
||||
("/home/fizz".to_string(), "fizz".to_string()),
|
||||
],
|
||||
search: None,
|
||||
last_suggestion: None,
|
||||
};
|
||||
|
||||
let s = states.suggest("/home/user");
|
||||
assert_eq!(Suggestion::Rescan(PathBuf::from("/home")), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_suggest_none() {
|
||||
let mut states = OwnStates {
|
||||
files: vec![
|
||||
("/home/foo".to_string(), "foo".to_string()),
|
||||
("/home/bar".to_string(), "bar".to_string()),
|
||||
("/home/buzz".to_string(), "buzz".to_string()),
|
||||
("/home/fizz".to_string(), "fizz".to_string()),
|
||||
],
|
||||
search: None,
|
||||
last_suggestion: None,
|
||||
};
|
||||
|
||||
let s = states.suggest("");
|
||||
assert_eq!(Suggestion::Suggest("/home/foo".to_string()), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_suggest_none_if_dot() {
|
||||
let mut states = OwnStates {
|
||||
files: vec![
|
||||
("/home/foo".to_string(), "foo".to_string()),
|
||||
("/home/bar".to_string(), "bar".to_string()),
|
||||
("/home/buzz".to_string(), "buzz".to_string()),
|
||||
("/home/fizz".to_string(), "fizz".to_string()),
|
||||
],
|
||||
search: None,
|
||||
last_suggestion: None,
|
||||
};
|
||||
|
||||
let s = states.suggest("./th");
|
||||
assert_eq!(Suggestion::None, s);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,8 @@ use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::props::{
|
||||
Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers,
|
||||
};
|
||||
use tuirealm::tui::layout::Corner;
|
||||
use tuirealm::tui::text::{Line, Span};
|
||||
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState};
|
||||
use tuirealm::tui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
|
||||
use tuirealm::{MockComponent, Props, State, StateValue};
|
||||
|
||||
pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
|
||||
@@ -235,7 +234,7 @@ impl MockComponent for FileList {
|
||||
// Make list
|
||||
let mut list = TuiList::new(list_items)
|
||||
.block(div)
|
||||
.start_corner(Corner::TopLeft);
|
||||
.direction(ListDirection::TopToBottom);
|
||||
if let Some(highlighted_color) = highlighted_color {
|
||||
list = list.highlight_style(
|
||||
Style::default()
|
||||
|
||||
@@ -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
|
||||
|
||||
use super::{Msg, TransferMsg, UiMsg};
|
||||
|
||||
mod file_list;
|
||||
use file_list::FileList;
|
||||
use tuirealm::command::{Cmd, Direction, Position};
|
||||
mod file_list_with_search;
|
||||
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{Alignment, Borders, Color, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
|
||||
use self::file_list::FileList;
|
||||
use self::file_list_with_search::FileListWithSearch;
|
||||
use super::{Msg, TransferMsg, UiMsg};
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerFuzzy {
|
||||
component: FileListWithSearch,
|
||||
}
|
||||
|
||||
impl ExplorerFuzzy {
|
||||
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
|
||||
Self {
|
||||
component: FileListWithSearch::default()
|
||||
.background(bg)
|
||||
.borders(Borders::default().color(hg))
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.title(title, Alignment::Left)
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_search(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Delete, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Delete);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Tab | Key::Up | Key::Down,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Change);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(ch),
|
||||
..
|
||||
}) => match self.perform(Cmd::Type(ch)) {
|
||||
CmdResult::Changed(State::One(StateValue::String(search))) => {
|
||||
Some(Msg::Ui(UiMsg::FuzzySearch(search)))
|
||||
}
|
||||
_ => Some(Msg::None),
|
||||
},
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseFindExplorer))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_file_list(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_DESELECT_ALL));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
self.perform(Cmd::Change);
|
||||
Some(Msg::None)
|
||||
}
|
||||
// -- comp msg
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseFindExplorer))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left | Key::Right,
|
||||
..
|
||||
}) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char(' '),
|
||||
..
|
||||
}) => Some(Msg::Transfer(TransferMsg::TransferFile)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Backspace,
|
||||
..
|
||||
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('b'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('e') | Key::Delete | Key::Function(8),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowDeletePopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('i'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('s') | Key::Function(2),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('v') | Key::Function(3),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Transfer(TransferMsg::OpenFile)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('w'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('z'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowChmodPopup)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ExplorerFuzzy {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match self.component.focus() {
|
||||
file_list_with_search::Focus::List => self.on_file_list(ev),
|
||||
file_list_with_search::Focus::Search => self.on_search(ev),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct ExplorerFind {
|
||||
@@ -261,7 +467,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('f'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
|
||||
}) => Some(Msg::Transfer(TransferMsg::InitFuzzySearch)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('g'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
@@ -457,7 +663,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('f'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
|
||||
}) => Some(Msg::Transfer(TransferMsg::InitFuzzySearch)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('g'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use nucleo::Utf32String;
|
||||
use remotefs::File;
|
||||
|
||||
use crate::explorer::builder::FileExplorerBuilder;
|
||||
use crate::explorer::{FileExplorer, FileSorting, GroupDirs};
|
||||
use crate::explorer::{FileExplorer, FileSorting};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
|
||||
const FUZZY_SEARCH_THRESHOLD: u16 = 50;
|
||||
|
||||
/// File explorer tab
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileExplorerTab {
|
||||
@@ -28,10 +31,10 @@ pub enum FoundExplorerTab {
|
||||
|
||||
/// Browser contains the browser options
|
||||
pub struct Browser {
|
||||
local: FileExplorer, // Local File explorer state
|
||||
remote: FileExplorer, // Remote File explorer state
|
||||
found: Option<(FoundExplorerTab, FileExplorer)>, // File explorer for find result
|
||||
tab: FileExplorerTab, // Current selected tab
|
||||
local: FileExplorer, // Local File explorer state
|
||||
remote: FileExplorer, // Remote File explorer state
|
||||
found: Option<Found>, // File explorer for find result
|
||||
tab: FileExplorerTab, // Current selected tab
|
||||
pub sync_browsing: bool,
|
||||
}
|
||||
|
||||
@@ -47,6 +50,16 @@ impl Browser {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn explorer(&self) -> &FileExplorer {
|
||||
match self.tab {
|
||||
FileExplorerTab::Local => &self.local,
|
||||
FileExplorerTab::Remote => &self.remote,
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
self.found.as_ref().map(|x| &x.explorer).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local(&self) -> &FileExplorer {
|
||||
&self.local
|
||||
}
|
||||
@@ -64,17 +77,35 @@ impl Browser {
|
||||
}
|
||||
|
||||
pub fn found(&self) -> Option<&FileExplorer> {
|
||||
self.found.as_ref().map(|x| &x.1)
|
||||
self.found.as_ref().map(|x| &x.explorer)
|
||||
}
|
||||
|
||||
pub fn found_mut(&mut self) -> Option<&mut FileExplorer> {
|
||||
self.found.as_mut().map(|x| &mut x.1)
|
||||
self.found.as_mut().map(|x| &mut x.explorer)
|
||||
}
|
||||
|
||||
/// Perform fuzzy search on found tab
|
||||
pub fn fuzzy_search(&mut self, needle: &str) {
|
||||
if let Some(x) = self.found.as_mut() {
|
||||
x.fuzzy_search(needle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize fuzzy search
|
||||
pub fn init_fuzzy_search(&mut self) {
|
||||
if let Some(explorer) = self.found_mut() {
|
||||
explorer.set_files(vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec<File>, wrkdir: &Path) {
|
||||
let mut explorer = Self::build_found_explorer(wrkdir);
|
||||
explorer.set_files(files);
|
||||
self.found = Some((tab, explorer));
|
||||
explorer.set_files(files.clone());
|
||||
self.found = Some(Found {
|
||||
tab,
|
||||
explorer,
|
||||
search_results: files,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn del_found(&mut self) {
|
||||
@@ -83,7 +114,7 @@ impl Browser {
|
||||
|
||||
/// Returns found tab if any
|
||||
pub fn found_tab(&self) -> Option<FoundExplorerTab> {
|
||||
self.found.as_ref().map(|x| x.0)
|
||||
self.found.as_ref().map(|x| x.tab)
|
||||
}
|
||||
|
||||
pub fn tab(&self) -> FileExplorerTab {
|
||||
@@ -129,8 +160,8 @@ impl Browser {
|
||||
/// Build explorer reading from `ConfigClient`, for found result (has some differences)
|
||||
fn build_found_explorer(wrkdir: &Path) -> FileExplorer {
|
||||
FileExplorerBuilder::new()
|
||||
.with_file_sorting(FileSorting::Name)
|
||||
.with_group_dirs(Some(GroupDirs::First))
|
||||
.with_file_sorting(FileSorting::None)
|
||||
.with_group_dirs(None)
|
||||
.with_hidden_files(true)
|
||||
.with_stack_size(0)
|
||||
.with_formatter(Some(
|
||||
@@ -139,3 +170,48 @@ impl Browser {
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/// Found state
|
||||
struct Found {
|
||||
explorer: FileExplorer,
|
||||
/// Search results; original copy of files
|
||||
search_results: Vec<File>,
|
||||
tab: FoundExplorerTab,
|
||||
}
|
||||
|
||||
impl Found {
|
||||
/// Fuzzy search from `search_results` and update `explorer.files` with the results.
|
||||
pub fn fuzzy_search(&mut self, needle: &str) {
|
||||
let search = Utf32String::from(needle);
|
||||
let mut nucleo = nucleo::Matcher::new(nucleo::Config::DEFAULT.match_paths());
|
||||
|
||||
// get scores
|
||||
let mut fuzzy_results_with_score = self
|
||||
.search_results
|
||||
.iter()
|
||||
.map(|f| {
|
||||
(
|
||||
Utf32String::from(f.path().to_string_lossy().into_owned()),
|
||||
f,
|
||||
)
|
||||
})
|
||||
.filter_map(|(path, file)| {
|
||||
nucleo
|
||||
.fuzzy_match(path.slice(..), search.slice(..))
|
||||
.map(|score| (path, file, score))
|
||||
})
|
||||
.filter(|(_, _, score)| *score >= FUZZY_SEARCH_THRESHOLD)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// sort by score; highest first
|
||||
fuzzy_results_with_score.sort_by(|(_, _, a), (_, _, b)| b.cmp(a));
|
||||
|
||||
// update files
|
||||
self.explorer.set_files(
|
||||
fuzzy_results_with_score
|
||||
.into_iter()
|
||||
.map(|(_, file, _)| file.clone())
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
|
||||
pub(crate) mod browser;
|
||||
pub(crate) mod transfer;
|
||||
pub(crate) mod walkdir;
|
||||
|
||||
4
src/ui/activities/filetransfer/lib/walkdir.rs
Normal file
4
src/ui/activities/filetransfer/lib/walkdir.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WalkdirStates {
|
||||
pub aborted: bool,
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const LOG_CAPACITY: usize = 256;
|
||||
impl FileTransferActivity {
|
||||
/// Call `Application::tick()` and process messages in `Update`
|
||||
pub(super) fn tick(&mut self) {
|
||||
match self.app.tick(PollStrategy::UpTo(3)) {
|
||||
match self.app.tick(PollStrategy::UpTo(1)) {
|
||||
Ok(messages) => {
|
||||
if !messages.is_empty() {
|
||||
self.redraw = true;
|
||||
@@ -111,7 +111,9 @@ impl FileTransferActivity {
|
||||
match &ft_params.params {
|
||||
ProtocolParams::Generic(params) => params.address.clone(),
|
||||
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
|
||||
ProtocolParams::Kube(params) => params.pod.clone(),
|
||||
ProtocolParams::Kube(params) => {
|
||||
params.namespace.clone().unwrap_or("default".to_string())
|
||||
}
|
||||
ProtocolParams::Smb(params) => params.address.clone(),
|
||||
ProtocolParams::WebDAV(params) => params.uri.clone(),
|
||||
}
|
||||
@@ -137,11 +139,9 @@ impl FileTransferActivity {
|
||||
format!("Connecting to {}…", params.bucket_name)
|
||||
}
|
||||
ProtocolParams::Kube(params) => {
|
||||
info!(
|
||||
"Client is not connected to remote; connecting to pod {}",
|
||||
params.pod,
|
||||
);
|
||||
format!("Connecting to {}…", params.pod)
|
||||
let namespace = params.namespace.as_deref().unwrap_or("default");
|
||||
info!("Client is not connected to remote; connecting to namespace {namespace}",);
|
||||
format!("Connecting to Kube namespace {namespace}…",)
|
||||
}
|
||||
ProtocolParams::Smb(params) => {
|
||||
info!(
|
||||
|
||||
@@ -14,6 +14,7 @@ mod view;
|
||||
|
||||
// locals
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
// Includes
|
||||
@@ -21,6 +22,7 @@ use chrono::{DateTime, Local};
|
||||
use lib::browser;
|
||||
use lib::browser::Browser;
|
||||
use lib::transfer::{TransferOpts, TransferStates};
|
||||
use lib::walkdir::WalkdirStates;
|
||||
use remotefs::RemoteFs;
|
||||
use session::TransferPayload;
|
||||
use tempfile::TempDir;
|
||||
@@ -50,7 +52,6 @@ enum Id {
|
||||
FatalPopup,
|
||||
FileInfoPopup,
|
||||
FilterPopup,
|
||||
FindPopup,
|
||||
FooterBar,
|
||||
GlobalListener,
|
||||
GotoPopup,
|
||||
@@ -94,6 +95,7 @@ enum PendingActionMsg {
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum TransferMsg {
|
||||
AbortWalkdir,
|
||||
AbortTransfer,
|
||||
Chmod(remotefs::fs::UnixPex),
|
||||
CopyFileTo(String),
|
||||
@@ -104,6 +106,7 @@ enum TransferMsg {
|
||||
GoTo(String),
|
||||
GoToParentDirectory,
|
||||
GoToPreviousDirectory,
|
||||
InitFuzzySearch,
|
||||
Mkdir(String),
|
||||
NewFile(String),
|
||||
OpenFile,
|
||||
@@ -111,8 +114,8 @@ enum TransferMsg {
|
||||
OpenTextFile,
|
||||
ReloadDir,
|
||||
RenameFile(String),
|
||||
RescanGotoFiles(PathBuf),
|
||||
SaveFileAs(String),
|
||||
SearchFile(String),
|
||||
ToggleWatch,
|
||||
ToggleWatchFor(usize),
|
||||
TransferFile,
|
||||
@@ -133,7 +136,6 @@ enum UiMsg {
|
||||
CloseFileSortingPopup,
|
||||
CloseFilterPopup,
|
||||
CloseFindExplorer,
|
||||
CloseFindPopup,
|
||||
CloseGotoPopup,
|
||||
CloseKeybindingsPopup,
|
||||
CloseMkdirPopup,
|
||||
@@ -147,6 +149,7 @@ enum UiMsg {
|
||||
CloseWatcherPopup,
|
||||
Disconnect,
|
||||
FilterFiles(String),
|
||||
FuzzySearch(String),
|
||||
LogBackTabbed,
|
||||
Quit,
|
||||
ReplacePopupTabbed,
|
||||
@@ -158,7 +161,6 @@ enum UiMsg {
|
||||
ShowFileInfoPopup,
|
||||
ShowFileSortingPopup,
|
||||
ShowFilterPopup,
|
||||
ShowFindPopup,
|
||||
ShowGotoPopup,
|
||||
ShowKeybindingsPopup,
|
||||
ShowLogPanel,
|
||||
@@ -219,6 +221,9 @@ pub struct FileTransferActivity {
|
||||
browser: Browser,
|
||||
/// Current log lines
|
||||
log_records: VecDeque<LogRecord>,
|
||||
/// Fuzzy search states
|
||||
walkdir: WalkdirStates,
|
||||
/// Transfer states
|
||||
transfer: TransferStates,
|
||||
/// Temporary directory where to store temporary stuff
|
||||
cache: Option<TempDir>,
|
||||
@@ -246,6 +251,7 @@ impl FileTransferActivity {
|
||||
client: Builder::build(params.protocol, params.params.clone(), &config_client),
|
||||
browser: Browser::new(&config_client),
|
||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||
walkdir: WalkdirStates::default(),
|
||||
transfer: TransferStates::default(),
|
||||
cache: match TempDir::new() {
|
||||
Ok(d) => Some(d),
|
||||
|
||||
@@ -57,7 +57,11 @@ impl FileTransferActivity {
|
||||
// Connect to remote
|
||||
match self.client.connect() {
|
||||
Ok(Welcome { banner, .. }) => {
|
||||
self.connected = true;
|
||||
self.connected = self.client.is_connected();
|
||||
if !self.connected {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(banner) = banner {
|
||||
// Log welcome
|
||||
self.log(
|
||||
@@ -68,6 +72,15 @@ impl FileTransferActivity {
|
||||
banner
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Log welcome
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Established connection with '{}'",
|
||||
self.get_remote_hostname()
|
||||
),
|
||||
);
|
||||
}
|
||||
// Try to change directory to entry directory
|
||||
let mut remote_chdir: Option<PathBuf> = None;
|
||||
@@ -87,7 +100,7 @@ impl FileTransferActivity {
|
||||
Err(err) => {
|
||||
// Set popup fatal error
|
||||
self.umount_wait();
|
||||
self.mount_fatal(&err.to_string());
|
||||
self.mount_fatal(err.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,16 +124,28 @@ impl FileTransferActivity {
|
||||
|
||||
/// Reload remote directory entries and update browser
|
||||
pub(super) fn reload_remote_dir(&mut self) {
|
||||
if !self.connected {
|
||||
return;
|
||||
}
|
||||
// Get current entries
|
||||
if let Ok(wrkdir) = self.client.pwd() {
|
||||
self.mount_blocking_wait("Loading remote directory...");
|
||||
|
||||
if self.remote_scan(wrkdir.as_path()).is_ok() {
|
||||
// Set wrkdir
|
||||
self.remote_mut().wrkdir = wrkdir;
|
||||
}
|
||||
let res = self.remote_scan(wrkdir.as_path());
|
||||
|
||||
self.umount_wait();
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
self.remote_mut().wrkdir = wrkdir;
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current remote directory: {err}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,29 +155,33 @@ impl FileTransferActivity {
|
||||
|
||||
let wrkdir: PathBuf = self.host.pwd();
|
||||
|
||||
if self.local_scan(wrkdir.as_path()).is_ok() {
|
||||
self.local_mut().wrkdir = wrkdir;
|
||||
}
|
||||
let res = self.local_scan(wrkdir.as_path());
|
||||
|
||||
self.umount_wait();
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
self.local_mut().wrkdir = wrkdir;
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current local directory: {err}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan current local directory
|
||||
fn local_scan(&mut self, path: &Path) -> Result<(), HostError> {
|
||||
match self.host.scan_dir(path) {
|
||||
match self.host.list_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.local_mut().set_files(files);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current directory: {err}"),
|
||||
);
|
||||
Err(err)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,13 +193,7 @@ impl FileTransferActivity {
|
||||
self.remote_mut().set_files(files);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current directory: {err}"),
|
||||
);
|
||||
Err(err)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,7 +358,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
// Get files in dir
|
||||
match self.host.scan_dir(entry.path()) {
|
||||
match self.host.list_dir(entry.path()) {
|
||||
Ok(entries) => {
|
||||
// Iterate over files
|
||||
for entry in entries.iter() {
|
||||
@@ -1133,7 +1156,7 @@ impl FileTransferActivity {
|
||||
fn get_total_transfer_size_local(&mut self, entry: &File) -> usize {
|
||||
if entry.is_dir() {
|
||||
// List dir
|
||||
match self.host.scan_dir(entry.path()) {
|
||||
match self.host.list_dir(entry.path()) {
|
||||
Ok(files) => files
|
||||
.iter()
|
||||
.map(|x| self.get_total_transfer_size_local(x))
|
||||
|
||||
@@ -8,6 +8,7 @@ use remotefs::fs::File;
|
||||
use tuirealm::props::{AttrValue, Attribute};
|
||||
use tuirealm::{State, StateValue, Update};
|
||||
|
||||
use super::actions::walkdir::WalkdirError;
|
||||
use super::actions::SelectedFile;
|
||||
use super::browser::{FileExplorerTab, FoundExplorerTab};
|
||||
use super::{ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg};
|
||||
@@ -32,6 +33,9 @@ impl FileTransferActivity {
|
||||
TransferMsg::AbortTransfer => {
|
||||
self.transfer.abort();
|
||||
}
|
||||
TransferMsg::AbortWalkdir => {
|
||||
self.walkdir.aborted = true;
|
||||
}
|
||||
TransferMsg::Chmod(mode) => {
|
||||
self.umount_chmod();
|
||||
self.mount_blocking_wait("Applying new file mode…");
|
||||
@@ -211,6 +215,59 @@ impl FileTransferActivity {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
TransferMsg::InitFuzzySearch => {
|
||||
// Mount wait
|
||||
self.mount_walkdir_wait();
|
||||
// Find
|
||||
let res: Result<Vec<File>, WalkdirError> = match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_walkdir_local(),
|
||||
FileExplorerTab::Remote => self.action_walkdir_remote(),
|
||||
_ => panic!("Trying to search for files, while already in a find result"),
|
||||
};
|
||||
// Umount wait
|
||||
self.umount_wait();
|
||||
// Match result
|
||||
match res {
|
||||
Err(WalkdirError::Error(err)) => {
|
||||
// Mount error
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
Err(WalkdirError::Aborted) => {
|
||||
self.mount_info("Search aborted");
|
||||
}
|
||||
Ok(files) if files.is_empty() => {
|
||||
// If no file has been found notify user
|
||||
self.mount_info("There are no files in the current directory");
|
||||
}
|
||||
Ok(files) => {
|
||||
// Get wrkdir
|
||||
let wrkdir = match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.local().wrkdir.clone(),
|
||||
_ => self.remote().wrkdir.clone(),
|
||||
};
|
||||
// Create explorer and load files
|
||||
self.browser.set_found(
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => FoundExplorerTab::Local,
|
||||
_ => FoundExplorerTab::Remote,
|
||||
},
|
||||
files,
|
||||
wrkdir.as_path(),
|
||||
);
|
||||
// init fuzzy search to display nothing
|
||||
self.browser.init_fuzzy_search();
|
||||
// Mount result widget
|
||||
self.mount_find(format!(r#"Searching at "{}""#, wrkdir.display()), true);
|
||||
self.update_find_list();
|
||||
// Initialize tab
|
||||
self.browser.change_tab(match self.browser.tab() {
|
||||
FileExplorerTab::Local => FileExplorerTab::FindLocal,
|
||||
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
|
||||
_ => FileExplorerTab::FindLocal,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
TransferMsg::Mkdir(dir) => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_mkdir(dir),
|
||||
@@ -267,6 +324,15 @@ impl FileTransferActivity {
|
||||
// Reload files
|
||||
self.update_browser_file_list()
|
||||
}
|
||||
TransferMsg::RescanGotoFiles(path) => {
|
||||
let files = self.action_scan(&path).unwrap_or_default();
|
||||
let files = files
|
||||
.into_iter()
|
||||
.filter(|f| f.is_dir() || f.is_symlink())
|
||||
.map(|f| f.path().to_string_lossy().to_string())
|
||||
.collect();
|
||||
self.update_goto(files);
|
||||
}
|
||||
TransferMsg::SaveFileAs(dest) => {
|
||||
self.umount_saveas();
|
||||
match self.browser.tab() {
|
||||
@@ -281,57 +347,7 @@ impl FileTransferActivity {
|
||||
// Reload files
|
||||
self.update_browser_file_list_swapped();
|
||||
}
|
||||
TransferMsg::SearchFile(search) => {
|
||||
self.umount_find_input();
|
||||
// Mount wait
|
||||
self.mount_blocking_wait(format!(r#"Searching for "{search}"…"#).as_str());
|
||||
// Find
|
||||
let res: Result<Vec<File>, String> = match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_find(search.clone()),
|
||||
FileExplorerTab::Remote => self.action_remote_find(search.clone()),
|
||||
_ => panic!("Trying to search for files, while already in a find result"),
|
||||
};
|
||||
// Umount wait
|
||||
self.umount_wait();
|
||||
// Match result
|
||||
match res {
|
||||
Err(err) => {
|
||||
// Mount error
|
||||
self.mount_error(err.as_str());
|
||||
}
|
||||
Ok(files) if files.is_empty() => {
|
||||
// If no file has been found notify user
|
||||
self.mount_info(
|
||||
format!(r#"Could not find any file matching "{search}""#).as_str(),
|
||||
);
|
||||
}
|
||||
Ok(files) => {
|
||||
// Get wrkdir
|
||||
let wrkdir = match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.local().wrkdir.clone(),
|
||||
_ => self.remote().wrkdir.clone(),
|
||||
};
|
||||
// Create explorer and load files
|
||||
self.browser.set_found(
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => FoundExplorerTab::Local,
|
||||
_ => FoundExplorerTab::Remote,
|
||||
},
|
||||
files,
|
||||
wrkdir.as_path(),
|
||||
);
|
||||
// Mount result widget
|
||||
self.mount_find(&search);
|
||||
self.update_find_list();
|
||||
// Initialize tab
|
||||
self.browser.change_tab(match self.browser.tab() {
|
||||
FileExplorerTab::Local => FileExplorerTab::FindLocal,
|
||||
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
|
||||
_ => FileExplorerTab::FindLocal,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TransferMsg::ToggleWatch => self.action_toggle_watch(),
|
||||
TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index),
|
||||
TransferMsg::TransferFile => {
|
||||
@@ -405,7 +421,6 @@ impl FileTransferActivity {
|
||||
self.finalize_find();
|
||||
self.umount_find();
|
||||
}
|
||||
UiMsg::CloseFindPopup => self.umount_find_input(),
|
||||
UiMsg::CloseGotoPopup => self.umount_goto(),
|
||||
UiMsg::CloseKeybindingsPopup => self.umount_help(),
|
||||
UiMsg::CloseMkdirPopup => self.umount_mkdir(),
|
||||
@@ -439,7 +454,7 @@ impl FileTransferActivity {
|
||||
wrkdir.as_path(),
|
||||
);
|
||||
// Mount result widget
|
||||
self.mount_find(&filter);
|
||||
self.mount_find(&filter, false);
|
||||
self.update_find_list();
|
||||
// Initialize tab
|
||||
self.browser.change_tab(match self.browser.tab() {
|
||||
@@ -448,6 +463,10 @@ impl FileTransferActivity {
|
||||
_ => FileExplorerTab::FindLocal,
|
||||
});
|
||||
}
|
||||
UiMsg::FuzzySearch(needle) => {
|
||||
self.browser.fuzzy_search(&needle);
|
||||
self.update_find_list();
|
||||
}
|
||||
UiMsg::ShowLogPanel => {
|
||||
assert!(self.app.active(&Id::Log).is_ok());
|
||||
}
|
||||
@@ -514,7 +533,6 @@ impl FileTransferActivity {
|
||||
}
|
||||
UiMsg::ShowFileSortingPopup => self.mount_file_sorting(),
|
||||
UiMsg::ShowFilterPopup => self.mount_filter(),
|
||||
UiMsg::ShowFindPopup => self.mount_find_input(),
|
||||
UiMsg::ShowGotoPopup => self.mount_goto(),
|
||||
UiMsg::ShowKeybindingsPopup => self.mount_help(),
|
||||
UiMsg::ShowMkdirPopup => self.mount_mkdir(),
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
// Ext
|
||||
use remotefs::fs::{File, UnixPex};
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{PropPayload, PropValue, TextSpan};
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::tui::widgets::Clear;
|
||||
use tuirealm::{Sub, SubClause, SubEventClause};
|
||||
use tuirealm::{AttrValue, Attribute, Sub, SubClause, SubEventClause};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::browser::{FileExplorerTab, FoundExplorerTab};
|
||||
use super::components::ATTR_FILES;
|
||||
use super::{components, Context, FileTransferActivity, Id};
|
||||
use crate::explorer::FileSorting;
|
||||
use crate::utils::ui::{Popup, Size};
|
||||
@@ -80,7 +82,7 @@ impl FileTransferActivity {
|
||||
self.refresh_remote_status_bar();
|
||||
// Update components
|
||||
self.update_local_filelist();
|
||||
self.update_remote_filelist();
|
||||
// self.update_remote_filelist();
|
||||
// Global listener
|
||||
self.mount_global_listener();
|
||||
// Give focus to local explorer
|
||||
@@ -177,11 +179,6 @@ impl FileTransferActivity {
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.app.view(&Id::FilterPopup, f, popup);
|
||||
} else if self.app.mounted(&Id::FindPopup) {
|
||||
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size());
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.app.view(&Id::FindPopup, f, popup);
|
||||
} else if self.app.mounted(&Id::GotoPopup) {
|
||||
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size());
|
||||
f.render_widget(Clear, popup);
|
||||
@@ -307,7 +304,15 @@ impl FileTransferActivity {
|
||||
// make popup
|
||||
self.app.view(&Id::ErrorPopup, f, popup);
|
||||
} else if self.app.mounted(&Id::WaitPopup) {
|
||||
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.size());
|
||||
let wait_popup_lines = self
|
||||
.app
|
||||
.query(&Id::WaitPopup, Attribute::Text)
|
||||
.map(|x| x.map(|x| x.unwrap_payload().unwrap_vec().len()))
|
||||
.unwrap_or_default()
|
||||
.unwrap_or(1) as u16;
|
||||
|
||||
let popup =
|
||||
Popup(Size::Percentage(50), Size::Unit(2 + wait_popup_lines)).draw_in(f.size());
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.app.view(&Id::WaitPopup, f, popup);
|
||||
@@ -397,6 +402,38 @@ impl FileTransferActivity {
|
||||
assert!(self.app.active(&Id::WaitPopup).is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn mount_walkdir_wait(&mut self) {
|
||||
let color = self.theme().misc_info_dialog;
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::WaitPopup,
|
||||
Box::new(components::WalkdirWaitPopup::new(
|
||||
"Scanning current directory…",
|
||||
color
|
||||
)),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::WaitPopup).is_ok());
|
||||
|
||||
self.view();
|
||||
}
|
||||
|
||||
pub(super) fn update_walkdir_entries(&mut self, entries: usize) {
|
||||
let text = format!("Scanning current directory… ({entries} items found)",);
|
||||
let _ = self.app.attr(
|
||||
&Id::WaitPopup,
|
||||
Attribute::Text,
|
||||
AttrValue::Payload(PropPayload::Vec(vec![
|
||||
PropValue::TextSpan(TextSpan::from(text)),
|
||||
PropValue::TextSpan(TextSpan::from("Press 'CTRL+C' to abort")),
|
||||
])),
|
||||
);
|
||||
|
||||
self.view();
|
||||
}
|
||||
|
||||
pub(super) fn mount_blocking_wait<S: AsRef<str>>(&mut self, text: S) {
|
||||
self.mount_wait(text);
|
||||
self.view();
|
||||
@@ -515,7 +552,7 @@ impl FileTransferActivity {
|
||||
let _ = self.app.umount(&Id::ExecPopup);
|
||||
}
|
||||
|
||||
pub(super) fn mount_find(&mut self, search: &str) {
|
||||
pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) {
|
||||
// Get color
|
||||
let (bg, fg, hg) = match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => (
|
||||
@@ -529,18 +566,29 @@ impl FileTransferActivity {
|
||||
self.theme().transfer_remote_explorer_highlighted,
|
||||
),
|
||||
};
|
||||
|
||||
// Mount component
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::ExplorerFind,
|
||||
Box::new(components::ExplorerFind::new(
|
||||
format!(r#"Search results for "{search}""#),
|
||||
&[],
|
||||
bg,
|
||||
fg,
|
||||
hg
|
||||
)),
|
||||
if fuzzy_search {
|
||||
Box::new(components::ExplorerFuzzy::new(
|
||||
msg.to_string(),
|
||||
&[],
|
||||
bg,
|
||||
fg,
|
||||
hg,
|
||||
))
|
||||
} else {
|
||||
Box::new(components::ExplorerFind::new(
|
||||
msg.to_string(),
|
||||
&[],
|
||||
bg,
|
||||
fg,
|
||||
hg,
|
||||
))
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
@@ -551,37 +599,41 @@ impl FileTransferActivity {
|
||||
let _ = self.app.umount(&Id::ExplorerFind);
|
||||
}
|
||||
|
||||
pub(super) fn mount_find_input(&mut self) {
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::FindPopup,
|
||||
Box::new(components::FindPopup::new(input_color)),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::FindPopup).is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn umount_find_input(&mut self) {
|
||||
// Umount input find
|
||||
let _ = self.app.umount(&Id::FindPopup);
|
||||
}
|
||||
|
||||
pub(super) fn mount_goto(&mut self) {
|
||||
// get files
|
||||
let files = self
|
||||
.browser
|
||||
.explorer()
|
||||
.iter_files()
|
||||
.filter(|f| f.is_dir() || f.is_symlink())
|
||||
.map(|f| f.path().to_string_lossy().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let input_color = self.theme().misc_input_dialog;
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::GotoPopup,
|
||||
Box::new(components::GoToPopup::new(input_color)),
|
||||
Box::new(components::GotoPopup::new(input_color, files)),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::GotoPopup).is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn update_goto(&mut self, files: Vec<String>) {
|
||||
let payload = files
|
||||
.into_iter()
|
||||
.map(PropValue::Str)
|
||||
.collect::<Vec<PropValue>>();
|
||||
|
||||
let _ = self.app.attr(
|
||||
&Id::GotoPopup,
|
||||
Attribute::Custom(ATTR_FILES),
|
||||
AttrValue::Payload(PropPayload::Vec(payload)),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn umount_goto(&mut self) {
|
||||
let _ = self.app.umount(&Id::GotoPopup);
|
||||
}
|
||||
@@ -1094,38 +1146,33 @@ impl FileTransferActivity {
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::SortingPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::FindPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::SyncBrowsingMkdirPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::SymlinkPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::SymlinkPopup,
|
||||
Id::WatcherPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::WatcherPopup,
|
||||
Id::WatchedPathsList,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::WatchedPathsList,
|
||||
Id::ChmodPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::ChmodPopup,
|
||||
Id::WaitPopup,
|
||||
)))),
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::FilterPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::WaitPopup,
|
||||
)))),
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::FilterPopup,
|
||||
)))),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
|
||||
@@ -124,7 +124,7 @@ impl SetupActivity {
|
||||
}
|
||||
|
||||
/// Create a new ssh key
|
||||
pub(super) fn action_new_ssh_key(&mut self) {
|
||||
pub(super) fn action_new_ssh_key(&mut self) -> Result<(), String> {
|
||||
// get parameters
|
||||
let host: String = match self.app.state(&Id::Ssh(IdSsh::SshHost)) {
|
||||
Ok(State::One(StateValue::String(host))) => host,
|
||||
@@ -148,29 +148,23 @@ impl SetupActivity {
|
||||
// Lock ports
|
||||
assert!(self.app.lock_ports().is_ok());
|
||||
// Write key to file
|
||||
match edit::edit(placeholder.as_bytes()) {
|
||||
let res = match edit::edit(placeholder.as_bytes()) {
|
||||
Ok(rsa_key) => {
|
||||
// Remove placeholder from `rsa_key`
|
||||
let rsa_key: String = rsa_key.as_str().replace(placeholder.as_str(), "");
|
||||
if rsa_key.is_empty() {
|
||||
// Report error: empty key
|
||||
self.mount_error("SSH key is empty!");
|
||||
Err("SSH key is empty!".to_string())
|
||||
} else {
|
||||
// Add key
|
||||
if let Err(err) =
|
||||
self.add_ssh_key(host.as_str(), username.as_str(), rsa_key.as_str())
|
||||
{
|
||||
self.mount_error(
|
||||
format!("Could not create new private key: {err}").as_str(),
|
||||
);
|
||||
}
|
||||
self.add_ssh_key(host.as_str(), username.as_str(), rsa_key.as_str())
|
||||
.map_err(|e| format!("Could not create new private key: {e}"))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
self.mount_error(format!("Could not write private key to file: {err}").as_str());
|
||||
Err(format!("Could not write private key to file: {err}"))
|
||||
}
|
||||
}
|
||||
};
|
||||
// Restore terminal
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Enter alternate mode
|
||||
@@ -187,6 +181,8 @@ impl SetupActivity {
|
||||
// Unlock ports
|
||||
assert!(self.app.unlock_ports().is_ok());
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// Given a component and a color, save the color into the theme
|
||||
|
||||
@@ -225,9 +225,14 @@ impl SetupActivity {
|
||||
}
|
||||
}
|
||||
SshMsg::SaveSshKey => {
|
||||
self.action_new_ssh_key();
|
||||
let res = self.action_new_ssh_key();
|
||||
self.umount_new_ssh_key();
|
||||
self.reload_ssh_keys();
|
||||
match res {
|
||||
Ok(_) => {
|
||||
self.reload_ssh_keys();
|
||||
}
|
||||
Err(err) => self.mount_error(&err),
|
||||
}
|
||||
}
|
||||
SshMsg::ShowDelSshKeyPopup => {
|
||||
self.mount_del_ssh_key();
|
||||
|
||||
@@ -30,7 +30,7 @@ impl Context {
|
||||
theme_provider: ThemeProvider,
|
||||
error: Option<String>,
|
||||
) -> Context {
|
||||
Context {
|
||||
let mut ctx = Context {
|
||||
bookmarks_client,
|
||||
config_client,
|
||||
ft_params: None,
|
||||
@@ -38,7 +38,13 @@ impl Context {
|
||||
terminal: TerminalBridge::new().expect("Could not initialize terminal"),
|
||||
theme_provider,
|
||||
error,
|
||||
}
|
||||
};
|
||||
|
||||
// Init terminal state
|
||||
let _ = ctx.terminal.enable_raw_mode();
|
||||
let _ = ctx.terminal.enter_alternate_screen();
|
||||
|
||||
ctx
|
||||
}
|
||||
|
||||
// -- getters
|
||||
@@ -107,6 +113,5 @@ impl Drop for Context {
|
||||
// Re-enable terminal stuff
|
||||
let _ = self.terminal.disable_raw_mode();
|
||||
let _ = self.terminal.leave_alternate_screen();
|
||||
let _ = self.terminal.clear_screen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +56,12 @@ static REMOTE_WEBDAV_OPT_REGEX: Lazy<Regex> =
|
||||
lazy_regex!(r"(?:([^:]+):)(?:(.+[^@])@)(?:([^/]+))(?:(.+))?");
|
||||
|
||||
/**
|
||||
* Regex matches: {container}@{pod}/{path}
|
||||
* - group 1: Container
|
||||
* - group 2: Pod
|
||||
* - group 3: Some(path) | None
|
||||
* Regex matches: {namespace}[@{cluster_url}]$/{path}
|
||||
* - group 1: Namespace
|
||||
* - group 3: Some(cluster_url) | None
|
||||
* - group 5: Some(path) | None
|
||||
*/
|
||||
static REMOTE_KUBE_OPT_REGEX: Lazy<Regex> = lazy_regex!(r"(?:(.+[^@])@)(?:([^/]+))(?:(.+))?");
|
||||
static REMOTE_KUBE_OPT_REGEX: Lazy<Regex> = lazy_regex!(r"(?:([^@]+))(@(?:([^$]+)))?(\$(?:(.+)))?");
|
||||
|
||||
/**
|
||||
* Regex matches:
|
||||
@@ -100,8 +100,7 @@ static REMOTE_SMB_OPT_REGEX: Lazy<Regex> =
|
||||
/**
|
||||
* Regex matches:
|
||||
* - group 1: Version
|
||||
* E.g. termscp-0.3.2 => 0.3.2
|
||||
* v0.4.0 => 0.4.0
|
||||
* E.g. termscp-0.3.2 => 0.3.2; v0.4.0 => 0.4.0
|
||||
*/
|
||||
static SEMVER_REGEX: Lazy<Regex> = lazy_regex!(r".*(:?[0-9]\.[0-9]\.[0-9])");
|
||||
|
||||
@@ -314,23 +313,15 @@ fn parse_s3_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
||||
fn parse_kube_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
||||
match REMOTE_KUBE_OPT_REGEX.captures(s) {
|
||||
Some(groups) => {
|
||||
let container: String = groups
|
||||
.get(1)
|
||||
.map(|x| x.as_str().to_string())
|
||||
.unwrap_or_default();
|
||||
let pod: String = groups
|
||||
.get(2)
|
||||
.map(|x| x.as_str().to_string())
|
||||
.unwrap_or_default();
|
||||
let namespace: Option<String> = groups.get(1).map(|x| x.as_str().to_string());
|
||||
let cluster_url: Option<String> = groups.get(3).map(|x| x.as_str().to_string());
|
||||
let remote_path: Option<PathBuf> =
|
||||
groups.get(3).map(|group| PathBuf::from(group.as_str()));
|
||||
groups.get(5).map(|group| PathBuf::from(group.as_str()));
|
||||
Ok(FileTransferParams::new(
|
||||
FileTransferProtocol::Kube,
|
||||
ProtocolParams::Kube(KubeProtocolParams {
|
||||
pod,
|
||||
container,
|
||||
namespace: None,
|
||||
cluster_url: None,
|
||||
namespace,
|
||||
cluster_url,
|
||||
username: None,
|
||||
client_cert: None,
|
||||
client_key: None,
|
||||
@@ -754,13 +745,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn should_parse_kube_address() {
|
||||
let result = parse_remote_opt("kube://alpine@my-pod/tmp").ok().unwrap();
|
||||
let result = parse_remote_opt("kube://my-namespace@http://localhost:1234$/tmp")
|
||||
.ok()
|
||||
.unwrap();
|
||||
let params = result.params.kube_params().unwrap();
|
||||
|
||||
assert_eq!(params.container.as_str(), "alpine");
|
||||
assert_eq!(params.pod.as_str(), "my-pod");
|
||||
assert_eq!(params.namespace, None);
|
||||
assert_eq!(params.cluster_url, None);
|
||||
assert_eq!(params.namespace, Some("my-namespace".to_string()));
|
||||
assert_eq!(params.cluster_url.as_deref(), Some("http://localhost:1234"));
|
||||
assert_eq!(params.username, None);
|
||||
assert_eq!(params.client_cert, None);
|
||||
assert_eq!(params.client_key, None);
|
||||
|
||||
Reference in New Issue
Block a user