14 Commits

Author SHA1 Message Date
veeso
f2efb25ad7 fix: 0.16.1
Some checks are pending
Install.sh / build (push) Waiting to run
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run
2024-11-12 12:34:26 +01:00
veeso
e45c3d5b4e fix: gg rust 1.82 for introducing a nice breaking change in config which was not mentioned in changelog
Some checks failed
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
2024-10-21 11:09:50 +02:00
veeso
69f821baef fix: cfg unix forbidden in rust .82
Some checks are pending
Install.sh / build (push) Waiting to run
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run
2024-10-21 10:32:50 +02:00
Christian Visintin
11559d0962 Merge pull request #302 from veeso/0.16
Some checks failed
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
0.16
2024-10-14 16:36:36 +02:00
veeso
79c33095fc fix: tiny ui issue 2024-10-14 16:00:15 +02:00
veeso
0ec6bcbcef fix: 0.16
Some checks failed
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Linux / build (push) Has been cancelled
2024-10-14 11:40:45 +02:00
Christian Visintin
3cde067339 feat: Show .. directory before all the others in the explorer to navigate to the parent dir (#301) 2024-10-14 11:28:02 +02:00
Christian Visintin
c05ef32270 fix: issue 292 New version alert was not displayed due to a semver regex issue. (#300) 2024-10-14 10:43:47 +02:00
Christian Visintin
7eb913ec7b fix: tuirealm 2.x (#299) 2024-10-14 09:55:52 +02:00
veeso
4e63093d25 fix: users from lock
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
2024-10-07 17:50:15 +02:00
Christian Visintin
e5d50698d2 fix: vergen (#296) 2024-10-07 17:49:52 +02:00
Christian Visintin
79c31c2d84 fix: Use uzers instead of the dead package users which has several vulnerabilities (#295) 2024-10-07 17:40:35 +02:00
Christian Visintin
aab266a661 285 feature request multi host (#293) 2024-10-07 16:27:45 +02:00
veeso
a4c9acb49f feat: version 0.16 2024-10-05 17:41:40 +02:00
104 changed files with 6423 additions and 3326 deletions

View File

@@ -1,6 +1,8 @@
# Changelog # Changelog
- [Changelog](#changelog) - [Changelog](#changelog)
- [0.16.1](#0161)
- [0.16.0](#0160)
- [0.15.0](#0150) - [0.15.0](#0150)
- [0.14.0](#0140) - [0.14.0](#0140)
- [0.13.0](#0130) - [0.13.0](#0130)
@@ -37,6 +39,31 @@
--- ---
## 0.16.1
Released on 12/11/2024
- Just fixed this: e45c3d5b4ef64653e5b6cc4f3703e3b67514306d
- `fix: gg rust 1.82 for introducing a nice breaking change in config which was not mentioned in changelog`
## 0.16.0
Released on 14/10/2024
- [**Multi Host support**](https://github.com/veeso/termscp/issues/285):
- Now it is possible to work on two different remotes `remote A -> remote B` instead of just `localhost -> remote`
- Cli arguments now accept an additional `remote-args` for the left panel.
- For more details read this issue <https://github.com/veeso/termscp/issues/285>.
- Change between auth forms with `<BACKTAB>`
- Bookmarks are automatically loaded into the last auth form.
- [Issue 289](https://github.com/veeso/termscp/issues/289): Use `uzers` instead of the dead package `users` which has several vulnerabilities
- [Issue 290](https://github.com/veeso/termscp/issues/290): Password prompt was broken
- [Issue 298](https://github.com/veeso/termscp/issues/298): tuirealm 2.x
- Fixed some performance issues where sometimes the app froze for a couple of seconds, thanks to this <https://github.com/veeso/tui-realm/pull/78>.
- [Issue 292](https://github.com/veeso/termscp/issues/292): New version alert was not displayed due to a semver regex issue.
- [Issue 291](https://github.com/veeso/termscp/issues/291): Show `..` directory before all the others in the explorer. If you click on it you'll go the parent directory (same as pressing `<U>`). No, you can't select it for transfers and it's actually been implemented in the worse way possible, because this little change would require a huge refactoring of the explorer component. I promise I will do it one day, but I dunno when.
- Logging: filter out messages not related to termscp or remotefs
## 0.15.0 ## 0.15.0
Released on 03/10/2024 Released on 03/10/2024

740
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ license = "MIT"
name = "termscp" name = "termscp"
readme = "README.md" readme = "README.md"
repository = "https://github.com/veeso/termscp" repository = "https://github.com/veeso/termscp"
version = "0.15.0" version = "0.16.1"
[package.metadata.rpm] [package.metadata.rpm]
package = "termscp" package = "termscp"
@@ -77,8 +77,8 @@ tempfile = "^3"
thiserror = "^1" thiserror = "^1"
tokio = { version = "=1.38.1", features = ["rt"] } tokio = { version = "=1.38.1", features = ["rt"] }
toml = "^0.8" toml = "^0.8"
tui-realm-stdlib = "^1.3" tui-realm-stdlib = "2"
tuirealm = "^1.9" tuirealm = "2"
unicode-width = "^0.2" unicode-width = "^0.2"
version-compare = "^0.2" version-compare = "^0.2"
whoami = "^1.5" whoami = "^1.5"
@@ -90,6 +90,8 @@ serial_test = "^3"
[build-dependencies] [build-dependencies]
cfg_aliases = "0.2" cfg_aliases = "0.2"
vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] }
[features] [features]
default = ["smb", "with-keyring"] default = ["smb", "with-keyring"]
@@ -110,7 +112,7 @@ remotefs-ssh = "^0.4"
[target."cfg(target_family = \"unix\")".dependencies] [target."cfg(target_family = \"unix\")".dependencies]
remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] } remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
remotefs-ssh = { version = "^0.4", features = ["ssh2-vendored"] } remotefs-ssh = { version = "^0.4", features = ["ssh2-vendored"] }
users = "0.11.0" uzers = "0.12"
[profile.dev] [profile.dev]
incremental = true incremental = true

View File

@@ -71,7 +71,7 @@
</p> </p>
<p align="center">Developed by <a href="https://veeso.dev/" target="_blank">@veeso</a></p> <p align="center">Developed by <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Current version: 0.15.0 (03/10/2024)</p> <p align="center">Current version: 0.16.1 12/11/2024</p>
<p align="center"> <p align="center">
<a href="https://opensource.org/licenses/MIT" <a href="https://opensource.org/licenses/MIT"

View File

@@ -1,16 +1,33 @@
use cfg_aliases::cfg_aliases; use cfg_aliases::cfg_aliases;
use vergen_git2::{BuildBuilder, CargoBuilder, Emitter, Git2Builder, RustcBuilder, SysinfoBuilder};
fn main() { fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup cfg aliases // Setup cfg aliases
cfg_aliases! { cfg_aliases! {
// Platforms // Platforms
macos: { target_os = "macos" }, macos: { target_os = "macos" },
linux: { target_os = "linux" }, linux: { target_os = "linux" },
unix: { target_family = "unix" }, posix: { target_family = "unix" },
windows: { target_family = "windows" }, win: { target_family = "windows" },
// exclusive features // exclusive features
smb: { all(feature = "smb", not( macos )) }, smb: { all(feature = "smb", not( macos )) },
smb_unix: { all(unix, feature = "smb", not(macos)) }, smb_unix: { all(unix, feature = "smb", not(macos)) },
smb_windows: { all(windows, feature = "smb") } smb_windows: { all(windows, feature = "smb") }
} }
let build = BuildBuilder::all_build()?;
let cargo = CargoBuilder::all_cargo()?;
let git2 = Git2Builder::all_git()?;
let rustc = RustcBuilder::all_rustc()?;
let si = SysinfoBuilder::all_sysinfo()?;
Emitter::default()
.add_instructions(&build)?
.add_instructions(&cargo)?
.add_instructions(&git2)?
.add_instructions(&rustc)?
.add_instructions(&si)?
.emit()?;
Ok(())
} }

View File

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

View File

@@ -62,11 +62,11 @@
termscp kann mit den folgenden Optionen gestartet werden: termscp kann mit den folgenden Optionen gestartet werden:
`termscp [Optionen]... [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [lokales-arbeitsverzeichnis]` `termscp [Optionen]... [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [lokales-arbeitsverzeichnis]`
ODER ODER
`termscp [Optionen]... -b [Lesezeichen-Name] [lokales-arbeitsverzeichnis]` `termscp [Optionen]... -b [Lesezeichen-Name] -b [Lesezeichen-Name] [lokales-arbeitsverzeichnis]`
- `-P, --password <Passwort>` wenn Adresse angegeben wird, ist das Passwort dieses Argument - `-P, --password <Passwort>` wenn Adresse angegeben wird, ist das Passwort dieses Argument
- `-b, --address-as-bookmark` löst das Adressargument als Lesezeichenname auf - `-b, --address-as-bookmark` löst das Adressargument als Lesezeichenname auf

View File

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

View File

@@ -39,11 +39,11 @@
termscp se puede iniciar con las siguientes opciones: termscp se puede iniciar con las siguientes opciones:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` `termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
OR OR
`termscp [options]... -b [bookmark-name] [local-wrkdir]` `termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
- `-P, --password <password>` si se proporciona la dirección, la contraseña será este argumento - `-P, --password <password>` si se proporciona la dirección, la contraseña será este argumento
- `-b, --address-as-bookmark` resuelve el argumento de la dirección como un nombre de marcador - `-b, --address-as-bookmark` resuelve el argumento de la dirección como un nombre de marcador

View File

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

View File

@@ -37,11 +37,11 @@
termscp peut être démarré avec les options suivantes : termscp peut être démarré avec les options suivantes :
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` `termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
ou ou
`termscp [options]... -b [bookmark-name] [local-wrkdir]` `termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
- `-P, --password <password>` si l'adresse est fournie, le mot de passe sera cet argument - `-P, --password <password>` si l'adresse est fournie, le mot de passe sera cet argument
- `-b, --address-as-bookmark` résoudre l'argument d'adresse en tant que nom de signet - `-b, --address-as-bookmark` résoudre l'argument d'adresse en tant que nom de signet

View File

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

View File

@@ -37,11 +37,11 @@
termscp può essere lanciato con questi argomenti: termscp può essere lanciato con questi argomenti:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` `termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
O O
`termscp [options]... -b [bookmark-name] [local-wrkdir]` `termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
- `-P, --password <password>` Se viene fornito l'argomento indirizzo, questa sarà la password utilizzata per autenticarsi - `-P, --password <password>` Se viene fornito l'argomento indirizzo, questa sarà la password utilizzata per autenticarsi
- `-b, --address-as-bookmark` risolve l'argomento indirizzo come nome di un segnalibro - `-b, --address-as-bookmark` risolve l'argomento indirizzo come nome di un segnalibro

View File

@@ -40,13 +40,15 @@
termscp can be started with the following options: termscp can be started with the following options:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` `termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
OR OR
`termscp [options]... -b [bookmark-name] [local-wrkdir]` `termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
- `-P, --password <password>` if address is provided, password will be this argument AND any combination of the two
- `-P, --password <password>` if address is provided, password will be this argument. A password *can* be specified for each remote provided. The order must be the same of the address argument. The use of this parameter is discouraged.
- `-b, --address-as-bookmark` resolve address argument as a bookmark name - `-b, --address-as-bookmark` resolve address argument as a bookmark name
- `-q, --quiet` Disable logging - `-q, --quiet` Disable logging
- `-v, --version` Print version info - `-v, --version` Print version info

View File

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

View File

@@ -40,11 +40,11 @@
O termscp pode ser iniciado com as seguintes opções: O termscp pode ser iniciado com as seguintes opções:
`termscp [opções]... [protocol://usuário@endereço:porta:diretório-trabalho] [diretório-trabalho-local]` `termscp [opções]... [protocol://usuário@endereço:porta:diretório-trabalho] [protocol://usuário@endereço:porta:diretório-trabalho] [diretório-trabalho-local]`
OU OU
`termscp [opções]... -b [nome-do-favorito] [diretório-trabalho-local]` `termscp [opções]... -b [nome-do-favorito] -b [nome-do-favorito] [diretório-trabalho-local]`
- `-P, --password <senha>` se o endereço for fornecido, a senha será este argumento - `-P, --password <senha>` se o endereço for fornecido, a senha será este argumento
- `-b, --address-as-bookmark` resolve o argumento do endereço como um nome de favorito - `-b, --address-as-bookmark` resolve o argumento do endereço como um nome de favorito

View File

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

View File

@@ -37,11 +37,11 @@
termscp启动时可以使用以下选项: termscp启动时可以使用以下选项:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` `termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
或作为 或作为
`termscp [options]... -b [bookmark-name] [local-wrkdir]` `termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
- `-P, --password <password>` 登陆密码 - `-P, --password <password>` 登陆密码
- `-b, --address-as-bookmark` 将地址参数解析为书签名称 - `-b, --address-as-bookmark` 将地址参数解析为书签名称

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,15 +2,17 @@
//! //!
//! `activity_manager` is the module which provides run methods and handling for activities //! `activity_manager` is the module which provides run methods and handling for activities
// Deps use std::env;
// Namespaces
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait; use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
use crate::filetransfer::{FileTransferParams, FileTransferProtocol}; use crate::cli::{Remote, RemoteArgs};
use crate::host::{HostError, Localhost}; use crate::filetransfer::{
FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
};
use crate::host::HostError;
use crate::system::bookmarks_client::BookmarksClient; use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
use crate::system::environment; use crate::system::environment;
@@ -30,6 +32,16 @@ pub enum NextActivity {
SetupActivity, SetupActivity,
} }
pub enum Host {
HostBridge,
Remote,
}
pub enum HostParams {
HostBridge(HostBridgeParams),
Remote(FileTransferParams),
}
/// The activity manager takes care of running activities and handling them until the application has ended /// The activity manager takes care of running activities and handling them until the application has ended
pub struct ActivityManager { pub struct ActivityManager {
context: Option<Context>, context: Option<Context>,
@@ -62,10 +74,100 @@ impl ActivityManager {
}) })
} }
/// Configure remote args
pub fn configure_remote_args(&mut self, remote_args: RemoteArgs) -> Result<(), String> {
// Set for host bridge
match remote_args.host_bridge {
Remote::Bookmark(params) => self.resolve_bookmark_name(
Host::HostBridge,
&params.name,
params.password.as_deref(),
),
Remote::Host(host_params) => self.set_host_params(
HostParams::HostBridge(HostBridgeParams::Remote(
host_params.file_transfer_params.protocol,
host_params.file_transfer_params.params,
)),
host_params.password.as_deref(),
),
Remote::None => self.set_host_params(
HostParams::HostBridge(HostBridgeParams::Localhost(
env::current_dir()
.map_err(|e| format!("Could not get current directory: {e}"))?,
)),
None,
),
}?;
// set remote
match remote_args.remote {
Remote::Bookmark(params) => {
self.resolve_bookmark_name(Host::Remote, &params.name, params.password.as_deref())
}
Remote::Host(host_params) => self.set_host_params(
HostParams::Remote(host_params.file_transfer_params),
host_params.password.as_deref(),
),
Remote::None => Ok(()),
}
}
/// Set file transfer params /// Set file transfer params
pub fn set_filetransfer_params( pub fn set_host_params(
&mut self, &mut self,
mut params: FileTransferParams, host: HostParams,
password: Option<&str>,
) -> Result<(), String> {
let (remote_local_path, remote_remote_path) = match &host {
HostParams::Remote(params) => (params.local_path.clone(), params.remote_path.clone()),
_ => (None, None),
};
let mut remote_params = match &host {
HostParams::HostBridge(HostBridgeParams::Remote(protocol, protocol_params)) => {
Some((*protocol, protocol_params.clone()))
}
HostParams::HostBridge(HostBridgeParams::Localhost(_)) => None,
HostParams::Remote(ft_params) => Some((ft_params.protocol, ft_params.params.clone())),
};
// Put params into the context
if let Some((protocol, params)) = remote_params.as_mut() {
self.resolve_password_for_protocol_params(*protocol, params, password)?;
}
match host {
HostParams::HostBridge(HostBridgeParams::Localhost(path)) => {
self.context
.as_mut()
.unwrap()
.set_host_bridge_params(HostBridgeParams::Localhost(path));
}
HostParams::HostBridge(HostBridgeParams::Remote(_, _)) => {
let (protocol, params) = remote_params.unwrap();
self.context
.as_mut()
.unwrap()
.set_host_bridge_params(HostBridgeParams::Remote(protocol, params));
}
HostParams::Remote(_) => {
let (protocol, params) = remote_params.unwrap();
let params = FileTransferParams {
local_path: remote_local_path,
remote_path: remote_remote_path,
protocol,
params,
};
self.context.as_mut().unwrap().set_remote_params(params);
}
}
Ok(())
}
fn resolve_password_for_protocol_params(
&mut self,
protocol: FileTransferProtocol,
params: &mut ProtocolParams,
password: Option<&str>, password: Option<&str>,
) -> Result<(), String> { ) -> Result<(), String> {
// Set password if provided // Set password if provided
@@ -73,13 +175,13 @@ impl ActivityManager {
if let Some(password) = password { if let Some(password) = password {
params.set_default_secret(password.to_string()); params.set_default_secret(password.to_string());
} else if matches!( } else if matches!(
params.protocol, protocol,
FileTransferProtocol::Scp | FileTransferProtocol::Sftp, FileTransferProtocol::Scp | FileTransferProtocol::Sftp,
) && params.params.generic_params().is_some() ) && params.generic_params().is_some()
{ {
// * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password // * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password
let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config()); let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config());
let generic_params = params.params.generic_params().unwrap(); let generic_params = params.generic_params().unwrap();
if storage if storage
.resolve( .resolve(
&generic_params.address, &generic_params.address,
@@ -94,7 +196,7 @@ impl ActivityManager {
"storage could not find any suitable key for {}... prompting for password", "storage could not find any suitable key for {}... prompting for password",
generic_params.address generic_params.address
); );
self.prompt_password(&mut params)?; self.prompt_password(params)?;
} else { } else {
debug!( debug!(
"a key is already set for {}; password is not required", "a key is already set for {}; password is not required",
@@ -102,17 +204,19 @@ impl ActivityManager {
); );
} }
} else { } else {
self.prompt_password(&mut params)?; self.prompt_password(params)?;
} }
} }
// Put params into the context
self.context.as_mut().unwrap().set_ftparams(params);
Ok(()) Ok(())
} }
/// Prompt user for password to set into params. /// Prompt user for password to set into params.
fn prompt_password(&self, params: &mut FileTransferParams) -> Result<(), String> { fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> {
match tty::read_secret_from_tty("Password: ") { let ctx = self.context.as_mut().unwrap();
let prompt = format!("Password for {}: ", params.host_name());
match tty::read_secret_from_tty(ctx.terminal(), prompt) {
Err(err) => Err(format!("Could not read password: {err}")), Err(err) => Err(format!("Could not read password: {err}")),
Ok(Some(secret)) => { Ok(Some(secret)) => {
debug!( debug!(
@@ -130,16 +234,28 @@ impl ActivityManager {
/// Returns error if bookmark is not found /// Returns error if bookmark is not found
pub fn resolve_bookmark_name( pub fn resolve_bookmark_name(
&mut self, &mut self,
host: Host,
bookmark_name: &str, bookmark_name: &str,
password: Option<&str>, password: Option<&str>,
) -> Result<(), String> { ) -> Result<(), String> {
if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() { if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() {
match bookmarks_client.get_bookmark(bookmark_name) { let params = match bookmarks_client.get_bookmark(bookmark_name) {
None => Err(format!( None => {
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"# return Err(format!(
)), r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
Some(params) => self.set_filetransfer_params(params, password), ))
} }
Some(params) => params,
};
let params = match host {
Host::Remote => HostParams::Remote(params),
Host::HostBridge => {
HostParams::HostBridge(HostBridgeParams::Remote(params.protocol, params.params))
}
};
self.set_host_params(params, password)
} else { } else {
Err(String::from( Err(String::from(
"Could not resolve bookmark name: bookmarks client not initialized", "Could not resolve bookmark name: bookmarks client not initialized",
@@ -226,15 +342,24 @@ impl ActivityManager {
fn run_filetransfer(&mut self) -> Option<NextActivity> { fn run_filetransfer(&mut self) -> Option<NextActivity> {
info!("Starting FileTransferActivity"); info!("Starting FileTransferActivity");
// Get context // Get context
let mut ctx: Context = match self.context.take() { let ctx: Context = match self.context.take() {
Some(ctx) => ctx, Some(ctx) => ctx,
None => { None => {
error!("Failed to start FileTransferActivity: context is None"); error!("Failed to start FileTransferActivity: context is None");
return None; return None;
} }
}; };
let host_bridge_params = match ctx.host_bridge_params() {
Some(params) => params.clone(),
None => {
error!("Failed to start FileTransferActivity: host bridge params is None");
return None;
}
};
// If ft params is None, return None // If ft params is None, return None
let ft_params: &FileTransferParams = match ctx.ft_params() { let remote_params: &FileTransferParams = match ctx.remote_params() {
Some(ft_params) => ft_params, Some(ft_params) => ft_params,
None => { None => {
error!("Failed to start FileTransferActivity: file transfer params is None"); error!("Failed to start FileTransferActivity: file transfer params is None");
@@ -242,28 +367,8 @@ impl ActivityManager {
} }
}; };
// get local path:
// - if set in file transfer params, get it from there
// - otherwise is env current dir
// - otherwise is /
let local_wrkdir = ft_params
.local_path
.clone()
.or(std::env::current_dir().ok())
.unwrap_or(PathBuf::from("/"));
// Prepare activity
let host: Localhost = match Localhost::new(local_wrkdir) {
Ok(host) => host,
Err(err) => {
// Set error in context
error!("Failed to initialize localhost: {}", err);
ctx.set_error(format!("Could not initialize localhost: {err}"));
return None;
}
};
let mut activity: FileTransferActivity = let mut activity: FileTransferActivity =
FileTransferActivity::new(host, ft_params, self.ticks); FileTransferActivity::new(host_bridge_params, remote_params, self.ticks);
// Prepare result // Prepare result
let result: Option<NextActivity>; let result: Option<NextActivity>;
// Create activity // Create activity

View File

@@ -2,13 +2,15 @@
//! //!
//! defines the types for main.rs types //! defines the types for main.rs types
mod remote;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use argh::FromArgs; use argh::FromArgs;
pub use remote::{Remote, RemoteArgs};
use crate::activity_manager::NextActivity; use crate::activity_manager::NextActivity;
use crate::filetransfer::FileTransferParams;
use crate::system::logging::LogLevel; use crate::system::logging::LogLevel;
pub enum Task { pub enum Task {
@@ -17,12 +19,14 @@ pub enum Task {
InstallUpdate, InstallUpdate,
} }
#[derive(FromArgs)] #[derive(Default, FromArgs)]
#[argh(description = " #[argh(description = "
where positional can be: where positional can be:
- [address] [local-wrkdir] - [address_a] [address_b] [local-wrkdir]
OR OR
- [bookmark-Name] [local-wrkdir] - -b [bookmark-name_1] -b [bookmark-name_2] [local-wrkdir]
and any combination of the above
Address syntax can be: Address syntax can be:
@@ -37,14 +41,15 @@ pub struct Args {
#[argh(subcommand)] #[argh(subcommand)]
pub nested: Option<ArgsSubcommands>, pub nested: Option<ArgsSubcommands>,
/// resolve address argument as a bookmark name /// resolve address argument as a bookmark name
#[argh(switch, short = 'b')] #[argh(option, short = 'b')]
pub address_as_bookmark: bool, pub bookmark: Vec<String>,
/// enable TRACE log level /// enable TRACE log level
#[argh(switch, short = 'D')] #[argh(switch, short = 'D')]
pub debug: bool, pub debug: bool,
/// provide password from CLI /// provide password from CLI; if you need to provide multiple passwords, use multiple -P flags.
/// In case just respect the order of the addresses
#[argh(option, short = 'P')] #[argh(option, short = 'P')]
pub password: Option<String>, pub password: Vec<String>,
/// disable logging /// disable logging
#[argh(switch, short = 'q')] #[argh(switch, short = 'q')]
pub quiet: bool, pub quiet: bool,
@@ -55,10 +60,7 @@ pub struct Args {
#[argh(switch, short = 'v')] #[argh(switch, short = 'v')]
pub version: bool, pub version: bool,
// -- positional // -- positional
#[argh( #[argh(positional, description = "address1 address2 local-wrkdir")]
positional,
description = "protocol://user@address:port:wrkdir local-wrkdir"
)]
pub positional: Vec<String>, pub positional: Vec<String>,
} }
@@ -90,7 +92,7 @@ pub struct LoadThemeArgs {
} }
pub struct RunOpts { pub struct RunOpts {
pub remote: Remote, pub remote: RemoteArgs,
pub ticks: Duration, pub ticks: Duration,
pub log_level: LogLevel, pub log_level: LogLevel,
pub task: Task, pub task: Task,
@@ -122,45 +124,10 @@ impl RunOpts {
impl Default for RunOpts { impl Default for RunOpts {
fn default() -> Self { fn default() -> Self {
Self { Self {
remote: Remote::None, remote: RemoteArgs::default(),
ticks: Duration::from_millis(10), ticks: Duration::from_millis(10),
log_level: LogLevel::Info, log_level: LogLevel::Info,
task: Task::Activity(NextActivity::Authentication), task: Task::Activity(NextActivity::Authentication),
} }
} }
} }
#[allow(clippy::large_enum_variant)]
pub enum Remote {
Bookmark(BookmarkParams),
Host(HostParams),
None,
}
pub struct BookmarkParams {
pub name: String,
pub password: Option<String>,
}
pub struct HostParams {
pub params: FileTransferParams,
pub password: Option<String>,
}
impl BookmarkParams {
pub fn new<S: AsRef<str>>(name: S, password: Option<S>) -> Self {
Self {
name: name.as_ref().to_string(),
password: password.map(|x| x.as_ref().to_string()),
}
}
}
impl HostParams {
pub fn new<S: AsRef<str>>(params: FileTransferParams, password: Option<S>) -> Self {
Self {
params,
password: password.map(|x| x.as_ref().to_string()),
}
}
}

271
src/cli/remote.rs Normal file
View File

@@ -0,0 +1,271 @@
use std::path::{Path, PathBuf};
use super::Args;
use crate::filetransfer::FileTransferParams;
use crate::utils;
/// Address type
enum AddrType {
Address,
Bookmark,
}
/// Args for remote connection
#[derive(Debug)]
pub struct RemoteArgs {
pub host_bridge: Remote,
pub remote: Remote,
pub local_dir: Option<PathBuf>,
}
impl Default for RemoteArgs {
fn default() -> Self {
Self {
host_bridge: Remote::None,
remote: Remote::None,
local_dir: None,
}
}
}
impl TryFrom<&Args> for RemoteArgs {
type Error = String;
fn try_from(args: &Args) -> Result<Self, Self::Error> {
let mut remote_args = RemoteArgs::default();
// validate arguments
match (args.bookmark.len(), args.positional.len()) {
(0, positional) if positional < 4 => Ok(()),
(1, positional) if positional < 3 => Ok(()),
(2, positional) if positional < 2 => Ok(()),
(_, _) => Err("Too many arguments".to_string()),
}?;
// parse bookmark first
let last_item_index = (args.bookmark.len() + args.positional.len())
.checked_sub(1)
.unwrap_or_default();
let mut hosts = vec![];
for (i, (addr_type, arg)) in args
.bookmark
.iter()
.map(|x| (AddrType::Bookmark, x))
.chain(args.positional.iter().map(|x| (AddrType::Address, x)))
.enumerate()
{
// check if has password
let password = args.password.get(i).cloned();
// check if is last item and so a possible local dir
if i == last_item_index && Path::new(arg).exists() {
remote_args.local_dir = Some(PathBuf::from(arg));
continue;
}
let remote = match addr_type {
AddrType::Address => Self::parse_remote_address(arg)
.map(|x| Remote::Host(HostParams::new(x, password)))?,
AddrType::Bookmark => Remote::Bookmark(BookmarkParams::new(arg, password.as_ref())),
};
// set remote
hosts.push(remote);
}
// set args based on hosts len
if hosts.len() == 1 {
remote_args.remote = hosts.pop().unwrap();
} else if hosts.len() == 2 {
remote_args.host_bridge = hosts.pop().unwrap();
remote_args.remote = hosts.pop().unwrap();
}
Ok(remote_args)
}
}
impl RemoteArgs {
/// Parse remote address
fn parse_remote_address(remote: &str) -> Result<FileTransferParams, String> {
utils::parser::parse_remote_opt(remote).map_err(|e| format!("Bad address option: {e}"))
}
}
/// Remote argument type
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Remote {
/// Bookmark name argument
Bookmark(BookmarkParams),
/// Host argument
Host(HostParams),
/// Unspecified
None,
}
impl Remote {
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
/// Bookmark parameters
#[derive(Debug)]
pub struct BookmarkParams {
/// bookmark name
pub name: String,
/// bookmark password
pub password: Option<String>,
}
/// Host parameters
#[derive(Debug)]
pub struct HostParams {
/// file transfer parameters
pub file_transfer_params: FileTransferParams,
/// host password specified in arguments
pub password: Option<String>,
}
impl BookmarkParams {
pub fn new<S: AsRef<str>>(name: S, password: Option<S>) -> Self {
Self {
name: name.as_ref().to_string(),
password: password.map(|x| x.as_ref().to_string()),
}
}
}
impl HostParams {
pub fn new<S: AsRef<str>>(params: FileTransferParams, password: Option<S>) -> Self {
Self {
file_transfer_params: params,
password: password.map(|x| x.as_ref().to_string()),
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_should_make_remote_args_from_args_one_remote() {
let args = Args {
positional: vec!["scp://host1".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::None));
assert!(matches!(remote_args.remote, Remote::Host(_)));
assert_eq!(remote_args.local_dir, None);
}
#[test]
fn test_should_make_remote_args_from_args_two_remotes() {
let args = Args {
positional: vec!["scp://host1".to_string(), "scp://host2".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
assert!(matches!(remote_args.remote, Remote::Host(_)));
assert_eq!(remote_args.local_dir, None);
}
#[test]
#[cfg(posix)]
fn test_should_make_remote_args_from_two_remotes_and_local_dir() {
let args = Args {
positional: vec![
"scp://host1".to_string(),
"scp://host2".to_string(),
"/home".to_string(),
],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
assert!(matches!(remote_args.remote, Remote::Host(_)));
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
}
#[test]
fn test_should_make_remote_args_from_args_one_bookmarks() {
let args = Args {
bookmark: vec!["foo".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::None));
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
assert_eq!(remote_args.local_dir, None);
}
#[test]
fn test_should_make_remote_args_from_args_two_bookmarks() {
let args = Args {
bookmark: vec!["foo".to_string(), "bar".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Bookmark(_)));
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
assert_eq!(remote_args.local_dir, None);
}
#[test]
#[cfg(posix)]
fn test_should_make_remote_args_from_two_bookmarks_and_local_dir() {
let args = Args {
bookmark: vec!["foo".to_string(), "bar".to_string()],
positional: vec!["/home".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Bookmark(_)));
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
}
#[test]
fn test_should_make_remote_args_from_one_bookmark_and_one_remote() {
let args = Args {
bookmark: vec!["foo".to_string()],
positional: vec!["scp://host1".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
assert_eq!(remote_args.local_dir, None);
}
#[test]
#[cfg(posix)]
fn test_should_make_remote_args_from_one_bookmark_and_one_remote_with_local_dir() {
let args = Args {
positional: vec!["scp://host1".to_string(), "/home".to_string()],
bookmark: vec!["foo".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
}
}

View File

@@ -108,9 +108,9 @@ impl From<FileTransferParams> for Bookmark {
smb: Some(SmbParams::from(params.clone())), smb: Some(SmbParams::from(params.clone())),
protocol, protocol,
address: Some(params.address), address: Some(params.address),
#[cfg(unix)] #[cfg(posix)]
port: Some(params.port), port: Some(params.port),
#[cfg(windows)] #[cfg(win)]
port: None, port: None,
username: params.username, username: params.username,
password: params.password, password: params.password,
@@ -159,7 +159,7 @@ impl From<Bookmark> for FileTransferParams {
let params = KubeProtocolParams::from(params); let params = KubeProtocolParams::from(params);
Self::new(bookmark.protocol, ProtocolParams::Kube(params)) Self::new(bookmark.protocol, ProtocolParams::Kube(params))
} }
#[cfg(unix)] #[cfg(posix)]
FileTransferProtocol::Smb => { FileTransferProtocol::Smb => {
let params = TransferSmbParams::new( let params = TransferSmbParams::new(
bookmark.address.unwrap_or_default(), bookmark.address.unwrap_or_default(),
@@ -172,7 +172,7 @@ impl From<Bookmark> for FileTransferParams {
Self::new(bookmark.protocol, ProtocolParams::Smb(params)) Self::new(bookmark.protocol, ProtocolParams::Smb(params))
} }
#[cfg(windows)] #[cfg(win)]
FileTransferProtocol::Smb => { FileTransferProtocol::Smb => {
let params = TransferSmbParams::new( let params = TransferSmbParams::new(
bookmark.address.unwrap_or_default(), bookmark.address.unwrap_or_default(),
@@ -521,7 +521,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(unix)] #[cfg(posix)]
fn should_get_ftparams_from_smb_bookmark() { fn should_get_ftparams_from_smb_bookmark() {
let bookmark: Bookmark = Bookmark { let bookmark: Bookmark = Bookmark {
protocol: FileTransferProtocol::Smb, protocol: FileTransferProtocol::Smb,
@@ -559,7 +559,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(windows)] #[cfg(win)]
fn should_get_ftparams_from_smb_bookmark() { fn should_get_ftparams_from_smb_bookmark() {
let bookmark: Bookmark = Bookmark { let bookmark: Bookmark = Bookmark {
protocol: FileTransferProtocol::Smb, protocol: FileTransferProtocol::Smb,

View File

@@ -9,7 +9,7 @@ pub struct SmbParams {
pub workgroup: Option<String>, pub workgroup: Option<String>,
} }
#[cfg(unix)] #[cfg(posix)]
impl From<TransferSmbParams> for SmbParams { impl From<TransferSmbParams> for SmbParams {
fn from(params: TransferSmbParams) -> Self { fn from(params: TransferSmbParams) -> Self {
Self { Self {
@@ -19,7 +19,7 @@ impl From<TransferSmbParams> for SmbParams {
} }
} }
#[cfg(windows)] #[cfg(win)]
impl From<TransferSmbParams> for SmbParams { impl From<TransferSmbParams> for SmbParams {
fn from(params: TransferSmbParams) -> Self { fn from(params: TransferSmbParams) -> Self {
Self { Self {

View File

@@ -112,7 +112,7 @@ mod tests {
use std::path::PathBuf; use std::path::PathBuf;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use tuirealm::tui::style::Color; use tuirealm::ratatui::style::Color;
use super::*; use super::*;
use crate::config::bookmarks::{Bookmark, KubeParams, S3Params, SmbParams, UserHosts}; use crate::config::bookmarks::{Bookmark, KubeParams, S3Params, SmbParams, UserHosts};
@@ -422,14 +422,14 @@ mod tests {
let host = hosts.bookmarks.get("smb").unwrap(); let host = hosts.bookmarks.get("smb").unwrap();
assert_eq!(host.address.as_deref().unwrap(), "localhost"); assert_eq!(host.address.as_deref().unwrap(), "localhost");
assert_eq!(host.port.unwrap(), 445); assert_eq!(host.port.unwrap(), 445);
#[cfg(unix)] #[cfg(posix)]
assert_eq!(host.username.as_deref().unwrap(), "test"); assert_eq!(host.username.as_deref().unwrap(), "test");
#[cfg(unix)] #[cfg(posix)]
assert_eq!(host.password.as_deref().unwrap(), "test"); assert_eq!(host.password.as_deref().unwrap(), "test");
let smb = host.smb.as_ref().unwrap(); let smb = host.smb.as_ref().unwrap();
assert_eq!(smb.share.as_str(), "temp"); assert_eq!(smb.share.as_str(), "temp");
#[cfg(unix)] #[cfg(posix)]
assert_eq!(smb.workgroup.as_deref().unwrap(), "test"); assert_eq!(smb.workgroup.as_deref().unwrap(), "test");
} }

View File

@@ -6,7 +6,7 @@
// ext // ext
use serde::de::Error as DeError; use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tuirealm::tui::style::Color; use tuirealm::ratatui::style::Color;
use crate::utils::fmt::fmt_color; use crate::utils::fmt::fmt_color;
use crate::utils::parser::parse_color; use crate::utils::parser::parse_color;

View File

@@ -11,8 +11,8 @@ use bytesize::ByteSize;
use lazy_regex::{Lazy, Regex}; use lazy_regex::{Lazy, Regex};
use remotefs::File; use remotefs::File;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
#[cfg(unix)] #[cfg(posix)]
use users::{get_group_by_gid, get_user_by_uid}; use uzers::{get_group_by_gid, get_user_by_uid};
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time}; use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
use crate::utils::path::diff_paths; use crate::utils::path::diff_paths;
@@ -211,7 +211,7 @@ impl Formatter {
_fmt_extra: Option<&String>, _fmt_extra: Option<&String>,
) -> String { ) -> String {
// Get username // Get username
#[cfg(unix)] #[cfg(posix)]
let group: String = match fsentry.metadata().gid { let group: String = match fsentry.metadata().gid {
Some(gid) => match get_group_by_gid(gid) { Some(gid) => match get_group_by_gid(gid) {
Some(user) => user.name().to_string_lossy().to_string(), Some(user) => user.name().to_string_lossy().to_string(),
@@ -219,7 +219,7 @@ impl Formatter {
}, },
None => 0.to_string(), None => 0.to_string(),
}; };
#[cfg(windows)] #[cfg(win)]
let group: String = match fsentry.metadata().gid { let group: String = match fsentry.metadata().gid {
Some(gid) => gid.to_string(), Some(gid) => gid.to_string(),
None => 0.to_string(), None => 0.to_string(),
@@ -420,7 +420,7 @@ impl Formatter {
_fmt_extra: Option<&String>, _fmt_extra: Option<&String>,
) -> String { ) -> String {
// Get username // Get username
#[cfg(unix)] #[cfg(posix)]
let username: String = match fsentry.metadata().uid { let username: String = match fsentry.metadata().uid {
Some(uid) => match get_user_by_uid(uid) { Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(), Some(user) => user.name().to_string_lossy().to_string(),
@@ -428,7 +428,7 @@ impl Formatter {
}, },
None => 0.to_string(), None => 0.to_string(),
}; };
#[cfg(windows)] #[cfg(win)]
let username: String = match fsentry.metadata().uid { let username: String = match fsentry.metadata().uid {
Some(uid) => uid.to_string(), Some(uid) => uid.to_string(),
None => 0.to_string(), None => 0.to_string(),
@@ -592,7 +592,7 @@ mod tests {
mode: Some(UnixPex::from(0o644)), mode: Some(UnixPex::from(0o644)),
}, },
}; };
#[cfg(unix)] #[cfg(posix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -600,7 +600,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(windows)] #[cfg(win)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -623,7 +623,7 @@ mod tests {
mode: Some(UnixPex::from(0o644)), mode: Some(UnixPex::from(0o644)),
}, },
}; };
#[cfg(unix)] #[cfg(posix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -631,7 +631,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(windows)] #[cfg(win)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -654,7 +654,7 @@ mod tests {
mode: None, mode: None,
}, },
}; };
#[cfg(unix)] #[cfg(posix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -662,7 +662,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(windows)] #[cfg(win)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -685,7 +685,7 @@ mod tests {
mode: None, mode: None,
}, },
}; };
#[cfg(unix)] #[cfg(posix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -693,7 +693,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(windows)] #[cfg(win)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -723,7 +723,7 @@ mod tests {
mode: Some(UnixPex::from(0o755)), mode: Some(UnixPex::from(0o755)),
}, },
}; };
#[cfg(unix)] #[cfg(posix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -731,7 +731,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(windows)] #[cfg(win)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -754,7 +754,7 @@ mod tests {
mode: None, mode: None,
}, },
}; };
#[cfg(unix)] #[cfg(posix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -762,7 +762,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(windows)] #[cfg(win)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -864,7 +864,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(unix)] #[cfg(posix)]
fn should_fmt_path() { fn should_fmt_path() {
let t: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let entry = File { let entry = File {
@@ -896,7 +896,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(unix)] #[cfg(posix)]
fn should_fmt_utf8_path() { fn should_fmt_utf8_path() {
let t: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let entry = File { let entry = File {

View File

@@ -519,7 +519,7 @@ mod tests {
mode: Some(UnixPex::from(0o644)), mode: Some(UnixPex::from(0o644)),
}, },
}; };
#[cfg(unix)] #[cfg(posix)]
assert_eq!( assert_eq!(
explorer.fmt_file(&entry), explorer.fmt_file(&entry),
format!( format!(
@@ -527,7 +527,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(windows)] #[cfg(win)]
assert_eq!( assert_eq!(
explorer.fmt_file(&entry), explorer.fmt_file(&entry),
format!( format!(

View File

@@ -0,0 +1,21 @@
use super::{HostBridgeParams, RemoteFsBuilder};
use crate::host::{HostBridge, Localhost, RemoteBridged};
use crate::system::config_client::ConfigClient;
pub struct HostBridgeBuilder;
impl HostBridgeBuilder {
/// Build Host Bridge from parms
///
/// if protocol and parameters are inconsistent, the function will panic.
pub fn build(params: HostBridgeParams, config_client: &ConfigClient) -> Box<dyn HostBridge> {
match params {
HostBridgeParams::Localhost(path) => {
Box::new(Localhost::new(path).expect("Failed to create Localhost"))
}
HostBridgeParams::Remote(protocol, params) => Box::new(RemoteBridged::from(
RemoteFsBuilder::build(protocol, params, config_client),
)),
}
}
}

View File

@@ -2,12 +2,14 @@
//! //!
//! `filetransfer` is the module which provides the file transfer protocols and remotefs builders //! `filetransfer` is the module which provides the file transfer protocols and remotefs builders
mod builder; mod host_bridge_builder;
pub mod params; pub mod params;
mod remotefs_builder;
// -- export types // -- export types
pub use builder::Builder; pub use host_bridge_builder::HostBridgeBuilder;
pub use params::{FileTransferParams, ProtocolParams}; pub use params::{FileTransferParams, HostBridgeParams, ProtocolParams};
pub use remotefs_builder::RemoteFsBuilder;
/// This enum defines the different transfer protocol available in termscp /// This enum defines the different transfer protocol available in termscp

View File

@@ -15,6 +15,24 @@ pub use self::smb::SmbParams;
pub use self::webdav::WebDAVProtocolParams; pub use self::webdav::WebDAVProtocolParams;
use super::FileTransferProtocol; use super::FileTransferProtocol;
/// Host bridge params
#[derive(Debug, Clone)]
pub enum HostBridgeParams {
/// Localhost with starting working directory
Localhost(PathBuf),
/// Remote host with protocol and file transfer params
Remote(FileTransferProtocol, ProtocolParams),
}
impl HostBridgeParams {
pub fn unwrap_protocol_params(&self) -> &ProtocolParams {
match self {
HostBridgeParams::Localhost(_) => panic!("Localhost has no protocol params"),
HostBridgeParams::Remote(_, params) => params,
}
}
}
/// Holds connection parameters for file transfers /// Holds connection parameters for file transfers
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FileTransferParams { pub struct FileTransferParams {
@@ -34,6 +52,43 @@ pub enum ProtocolParams {
WebDAV(WebDAVProtocolParams), WebDAV(WebDAVProtocolParams),
} }
impl ProtocolParams {
pub fn password_missing(&self) -> bool {
match self {
ProtocolParams::AwsS3(params) => params.password_missing(),
ProtocolParams::Generic(params) => params.password_missing(),
ProtocolParams::Kube(params) => params.password_missing(),
ProtocolParams::Smb(params) => params.password_missing(),
ProtocolParams::WebDAV(params) => params.password_missing(),
}
}
/// Set the secret to ft params for the default secret field for this protocol
pub fn set_default_secret(&mut self, secret: String) {
match self {
ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
ProtocolParams::Generic(params) => params.set_default_secret(secret),
ProtocolParams::Kube(params) => params.set_default_secret(secret),
ProtocolParams::Smb(params) => params.set_default_secret(secret),
ProtocolParams::WebDAV(params) => params.set_default_secret(secret),
}
}
pub fn host_name(&self) -> String {
match self {
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::Kube(params) => params
.namespace
.as_ref()
.cloned()
.unwrap_or_else(|| String::from("default")),
ProtocolParams::Smb(params) => params.address.clone(),
ProtocolParams::WebDAV(params) => params.uri.clone(),
}
}
}
/// Protocol params used by most common protocols /// Protocol params used by most common protocols
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct GenericProtocolParams { pub struct GenericProtocolParams {
@@ -68,25 +123,15 @@ impl FileTransferParams {
/// Returns whether a password is supposed to be required for this protocol params. /// Returns whether a password is supposed to be required for this protocol params.
/// The result true is returned ONLY if the supposed secret is MISSING!!! /// The result true is returned ONLY if the supposed secret is MISSING!!!
#[cfg(test)]
pub fn password_missing(&self) -> bool { pub fn password_missing(&self) -> bool {
match &self.params { self.params.password_missing()
ProtocolParams::AwsS3(params) => params.password_missing(),
ProtocolParams::Generic(params) => params.password_missing(),
ProtocolParams::Kube(params) => params.password_missing(),
ProtocolParams::Smb(params) => params.password_missing(),
ProtocolParams::WebDAV(params) => params.password_missing(),
}
} }
/// Set the secret to ft params for the default secret field for this protocol /// Set the secret to ft params for the default secret field for this protocol
#[cfg(test)]
pub fn set_default_secret(&mut self, secret: String) { pub fn set_default_secret(&mut self, secret: String) {
match &mut self.params { self.params.set_default_secret(secret);
ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
ProtocolParams::Generic(params) => params.set_default_secret(secret),
ProtocolParams::Kube(params) => params.set_default_secret(secret),
ProtocolParams::Smb(params) => params.set_default_secret(secret),
ProtocolParams::WebDAV(params) => params.set_default_secret(secret),
}
} }
} }
@@ -317,7 +362,7 @@ mod test {
} }
#[test] #[test]
#[cfg(unix)] #[cfg(posix)]
fn set_default_secret_smb() { fn set_default_secret_smb() {
let mut params = FileTransferParams::new( let mut params = FileTransferParams::new(
FileTransferProtocol::Scp, FileTransferProtocol::Scp,

View File

@@ -2,12 +2,12 @@
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SmbParams { pub struct SmbParams {
pub address: String, pub address: String,
#[cfg(unix)] #[cfg(posix)]
pub port: u16, pub port: u16,
pub share: String, pub share: String,
pub username: Option<String>, pub username: Option<String>,
pub password: Option<String>, pub password: Option<String>,
#[cfg(unix)] #[cfg(posix)]
pub workgroup: Option<String>, pub workgroup: Option<String>,
} }
@@ -18,17 +18,17 @@ impl SmbParams {
pub fn new<S: AsRef<str>>(address: S, share: S) -> Self { pub fn new<S: AsRef<str>>(address: S, share: S) -> Self {
Self { Self {
address: address.as_ref().to_string(), address: address.as_ref().to_string(),
#[cfg(unix)] #[cfg(posix)]
port: 445, port: 445,
share: share.as_ref().to_string(), share: share.as_ref().to_string(),
username: None, username: None,
password: None, password: None,
#[cfg(unix)] #[cfg(posix)]
workgroup: None, workgroup: None,
} }
} }
#[cfg(unix)] #[cfg(posix)]
pub fn port(mut self, port: u16) -> Self { pub fn port(mut self, port: u16) -> Self {
self.port = port; self.port = port;
self self
@@ -44,7 +44,7 @@ impl SmbParams {
self self
} }
#[cfg(unix)] #[cfg(posix)]
pub fn workgroup(mut self, workgroup: Option<impl ToString>) -> Self { pub fn workgroup(mut self, workgroup: Option<impl ToString>) -> Self {
self.workgroup = workgroup.map(|x| x.to_string()); self.workgroup = workgroup.map(|x| x.to_string());
self self
@@ -57,12 +57,12 @@ impl SmbParams {
} }
/// Set password /// Set password
#[cfg(unix)] #[cfg(posix)]
pub fn set_default_secret(&mut self, secret: String) { pub fn set_default_secret(&mut self, secret: String) {
self.password = Some(secret); self.password = Some(secret);
} }
#[cfg(windows)] #[cfg(win)]
pub fn set_default_secret(&mut self, _secret: String) {} pub fn set_default_secret(&mut self, _secret: String) {}
} }
@@ -78,20 +78,20 @@ mod test {
let params = SmbParams::new("localhost", "temp"); let params = SmbParams::new("localhost", "temp");
assert_eq!(&params.address, "localhost"); assert_eq!(&params.address, "localhost");
#[cfg(unix)] #[cfg(posix)]
assert_eq!(params.port, 445); assert_eq!(params.port, 445);
assert_eq!(&params.share, "temp"); assert_eq!(&params.share, "temp");
#[cfg(unix)] #[cfg(posix)]
assert!(params.username.is_none()); assert!(params.username.is_none());
#[cfg(unix)] #[cfg(posix)]
assert!(params.password.is_none()); assert!(params.password.is_none());
#[cfg(unix)] #[cfg(posix)]
assert!(params.workgroup.is_none()); assert!(params.workgroup.is_none());
} }
#[test] #[test]
#[cfg(unix)] #[cfg(posix)]
fn should_init_smb_params_with_optionals() { fn should_init_smb_params_with_optionals() {
let params = SmbParams::new("localhost", "temp") let params = SmbParams::new("localhost", "temp")
.port(3456) .port(3456)
@@ -108,7 +108,7 @@ mod test {
} }
#[test] #[test]
#[cfg(windows)] #[cfg(win)]
fn should_init_smb_params_with_optionals() { fn should_init_smb_params_with_optionals() {
let params = SmbParams::new("localhost", "temp") let params = SmbParams::new("localhost", "temp")
.username(Some("foo")) .username(Some("foo"))

View File

@@ -27,9 +27,9 @@ use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::ssh as ssh_utils; use crate::utils::ssh as ssh_utils;
/// Remotefs builder /// Remotefs builder
pub struct Builder; pub struct RemoteFsBuilder;
impl Builder { impl RemoteFsBuilder {
/// Build RemoteFs client from protocol and params. /// Build RemoteFs client from protocol and params.
/// ///
/// if protocol and parameters are inconsistent, the function will panic. /// if protocol and parameters are inconsistent, the function will panic.
@@ -262,7 +262,7 @@ mod test {
.session_token(Some("gerry-scotti")), .session_token(Some("gerry-scotti")),
); );
let config_client = get_config_client(); let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client); let _ = RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client);
} }
#[test] #[test]
@@ -275,7 +275,7 @@ mod test {
.password(Some("qwerty123")), .password(Some("qwerty123")),
); );
let config_client = get_config_client(); let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::Ftp(true), params, &config_client); let _ = RemoteFsBuilder::build(FileTransferProtocol::Ftp(true), params, &config_client);
} }
#[test] #[test]
@@ -288,7 +288,7 @@ mod test {
client_key: Some("client_key".to_string()), client_key: Some("client_key".to_string()),
}); });
let config_client = get_config_client(); let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::Kube, params, &config_client); let _ = RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client);
} }
#[test] #[test]
@@ -301,7 +301,7 @@ mod test {
.password(Some("qwerty123")), .password(Some("qwerty123")),
); );
let config_client = get_config_client(); let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::Scp, params, &config_client); let _ = RemoteFsBuilder::build(FileTransferProtocol::Scp, params, &config_client);
} }
#[test] #[test]
@@ -314,7 +314,7 @@ mod test {
.password(Some("qwerty123")), .password(Some("qwerty123")),
); );
let config_client = get_config_client(); let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client); let _ = RemoteFsBuilder::build(FileTransferProtocol::Sftp, params, &config_client);
} }
#[test] #[test]
@@ -322,7 +322,7 @@ mod test {
fn should_build_smb_fs() { fn should_build_smb_fs() {
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share")); let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
let config_client = get_config_client(); let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::Smb, params, &config_client); let _ = RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client);
} }
#[test] #[test]
@@ -336,7 +336,7 @@ mod test {
.password(Some("qwerty123")), .password(Some("qwerty123")),
); );
let config_client = get_config_client(); let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client); let _ = RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client);
} }
fn get_config_client() -> ConfigClient { fn get_config_client() -> ConfigClient {

84
src/host/bridge.rs Normal file
View File

@@ -0,0 +1,84 @@
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use remotefs::fs::{Metadata, UnixPex};
use remotefs::File;
use super::HostResult;
/// Trait to bridge a remote filesystem to the host filesystem
///
/// In case of `Localhost` this should be effortless, while for remote hosts this should
/// implement a real bridge when the resource is first loaded on the local
/// filesystem and then processed on the remote.
pub trait HostBridge {
/// Connect to host
fn connect(&mut self) -> HostResult<()>;
/// Disconnect from host
fn disconnect(&mut self) -> HostResult<()>;
/// Returns whether the host is connected
fn is_connected(&mut self) -> bool;
/// Returns whether the host is localhost
fn is_localhost(&self) -> bool;
/// Print working directory
fn pwd(&mut self) -> HostResult<PathBuf>;
/// Change working directory with the new provided directory
fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult<PathBuf>;
/// Make a directory at path and update the file list (only if relative)
fn mkdir(&mut self, dir_name: &Path) -> HostResult<()> {
self.mkdir_ex(dir_name, false)
}
/// Extended option version of makedir.
/// ignex: don't report error if directory already exists
fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()>;
/// Remove file entry
fn remove(&mut self, entry: &File) -> HostResult<()>;
/// Rename file or directory to new name
fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()>;
/// Copy file to destination path
fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()>;
/// Stat file and create a File
fn stat(&mut self, path: &Path) -> HostResult<File>;
/// Returns whether provided file path exists
fn exists(&mut self, path: &Path) -> HostResult<bool>;
/// Get content of a directory
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>>;
/// Set file stat
fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()>;
/// Execute a command on localhost
fn exec(&mut self, cmd: &str) -> HostResult<String>;
/// Create a symlink from src to dst
fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()>;
/// Change file mode to file, according to UNIX permissions
fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()>;
/// Open file for reading
fn open_file(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>>;
/// Open file for writing
fn create_file(
&mut self,
file: &Path,
metadata: &Metadata,
) -> HostResult<Box<dyn Write + Send>>;
/// Finalize write operation
fn finalize_write(&mut self, writer: Box<dyn Write + Send>) -> HostResult<()>;
}

1042
src/host/localhost.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

211
src/host/remote_bridged.rs Normal file
View File

@@ -0,0 +1,211 @@
mod temp_mapped_file;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use remotefs::fs::{Metadata, UnixPex};
use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs};
use self::temp_mapped_file::TempMappedFile;
use super::{HostBridge, HostError, HostResult};
struct WriteStreamOp {
path: PathBuf,
metadata: Metadata,
tempfile: TempMappedFile,
}
/// A remote host bridged over the local host
pub struct RemoteBridged {
/// Remote fs client
remote: Box<dyn RemoteFs>,
/// Reminder used to finalize write stream
write_stream_op: Option<WriteStreamOp>,
}
impl RemoteBridged {
fn open_file_from_temp(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
let mut temp_file = TempMappedFile::new()?;
self.remote
.open_file(file, Box::new(temp_file.clone()))
.map_err(HostError::from)?;
// Sync changes
temp_file.sync()?;
// now return as read
Ok(Box::new(temp_file))
}
}
impl From<Box<dyn RemoteFs>> for RemoteBridged {
fn from(remote: Box<dyn RemoteFs>) -> Self {
RemoteBridged {
remote,
write_stream_op: None,
}
}
}
impl HostBridge for RemoteBridged {
fn connect(&mut self) -> HostResult<()> {
self.remote.connect().map(|_| ()).map_err(HostError::from)
}
fn disconnect(&mut self) -> HostResult<()> {
self.remote.disconnect().map_err(HostError::from)
}
fn is_connected(&mut self) -> bool {
self.remote.is_connected()
}
fn is_localhost(&self) -> bool {
false
}
fn pwd(&mut self) -> HostResult<PathBuf> {
debug!("Getting working directory");
self.remote.pwd().map_err(HostError::from)
}
fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult<PathBuf> {
debug!("Changing working directory to {:?}", new_dir);
self.remote.change_dir(new_dir).map_err(HostError::from)
}
fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()> {
debug!("Creating directory {:?}", dir_name);
match self.remote.create_dir(dir_name, UnixPex::from(0o755)) {
Ok(_) => Ok(()),
Err(remotefs::RemoteError {
kind: RemoteErrorType::DirectoryAlreadyExists,
..
}) if ignore_existing => Ok(()),
Err(e) => Err(HostError::from(e)),
}
}
fn remove(&mut self, entry: &File) -> HostResult<()> {
debug!("Removing {:?}", entry.path());
if entry.is_dir() {
self.remote
.remove_dir_all(entry.path())
.map_err(HostError::from)
} else {
self.remote
.remove_file(entry.path())
.map_err(HostError::from)
}
}
fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()> {
debug!("Renaming {:?} to {:?}", entry.path(), dst_path);
self.remote
.mov(entry.path(), dst_path)
.map_err(HostError::from)
}
fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()> {
debug!("Copying {:?} to {:?}", entry.path(), dst);
self.remote.copy(entry.path(), dst).map_err(HostError::from)
}
fn stat(&mut self, path: &Path) -> HostResult<File> {
debug!("Statting {:?}", path);
self.remote.stat(path).map_err(HostError::from)
}
fn exists(&mut self, path: &Path) -> HostResult<bool> {
debug!("Checking existence of {:?}", path);
self.remote.exists(path).map_err(HostError::from)
}
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
debug!("Listing directory {:?}", path);
self.remote.list_dir(path).map_err(HostError::from)
}
fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> {
debug!("Setting metadata for {:?}", path);
self.remote
.setstat(path, metadata.clone())
.map_err(HostError::from)
}
fn exec(&mut self, cmd: &str) -> HostResult<String> {
debug!("Executing command: {}", cmd);
self.remote
.exec(cmd)
.map(|(_, stdout)| stdout)
.map_err(HostError::from)
}
fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()> {
debug!("Creating symlink from {:?} to {:?}", src, dst);
self.remote.symlink(src, dst).map_err(HostError::from)
}
fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()> {
debug!("Changing permissions of {:?} to {:?}", path, pex);
let stat = self.remote.stat(path).map_err(HostError::from)?;
let mut metadata = stat.metadata.clone();
metadata.mode = Some(pex);
self.setstat(path, &metadata)
}
fn open_file(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
// try to use stream, otherwise download to a temporary file and return a reader
match self.remote.open(file) {
Ok(stream) => Ok(Box::new(stream)),
Err(RemoteError {
kind: RemoteErrorType::UnsupportedFeature,
..
}) => self.open_file_from_temp(file),
Err(e) => Err(HostError::from(e)),
}
}
fn create_file(
&mut self,
file: &Path,
metadata: &Metadata,
) -> HostResult<Box<dyn Write + Send>> {
// try to use stream, otherwise download to a temporary file and return a reader
match self.remote.create(file, metadata) {
Ok(stream) => Ok(Box::new(stream)),
Err(RemoteError {
kind: RemoteErrorType::UnsupportedFeature,
..
}) => {
let tempfile = TempMappedFile::new()?;
self.write_stream_op = Some(WriteStreamOp {
path: file.to_path_buf(),
metadata: metadata.clone(),
tempfile: tempfile.clone(),
});
Ok(Box::new(tempfile))
}
Err(e) => Err(HostError::from(e)),
}
}
fn finalize_write(&mut self, _writer: Box<dyn Write + Send>) -> HostResult<()> {
if let Some(WriteStreamOp {
path,
metadata,
mut tempfile,
}) = self.write_stream_op.take()
{
// sync
tempfile.sync()?;
// write file
self.remote
.create_file(&path, &metadata, Box::new(tempfile))?;
}
Ok(())
}
}

View File

@@ -0,0 +1,120 @@
use std::fs::File;
use std::io::{self, Read, Write};
use std::sync::{Arc, Mutex};
use tempfile::NamedTempFile;
use crate::host::{HostError, HostErrorType, HostResult};
/// A temporary file mapped to a remote file which has been transferred to local
/// and which supports read/write operations
#[derive(Debug, Clone)]
pub struct TempMappedFile {
tempfile: Arc<NamedTempFile>,
handle: Arc<Mutex<Option<File>>>,
}
impl Write for TempMappedFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let rc = self.write_hnd()?;
let mut ref_mut = rc.lock().unwrap();
ref_mut.as_mut().unwrap().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
let rc = self.write_hnd()?;
let mut ref_mut = rc.lock().unwrap();
ref_mut.as_mut().unwrap().flush()
}
}
impl Read for TempMappedFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let rc = self.read_hnd()?;
let mut ref_mut = rc.lock().unwrap();
ref_mut.as_mut().unwrap().read(buf)
}
}
impl TempMappedFile {
pub fn new() -> HostResult<Self> {
NamedTempFile::new()
.map(|tempfile| TempMappedFile {
tempfile: Arc::new(tempfile),
handle: Arc::new(Mutex::new(None)),
})
.map_err(|e| {
HostError::new(
HostErrorType::CouldNotCreateFile,
Some(e),
std::path::Path::new(""),
)
})
}
/// Syncs the file to disk and frees the file handle.
///
/// Must be called
pub fn sync(&mut self) -> HostResult<()> {
{
let mut lock = self.handle.lock().unwrap();
if let Some(hnd) = lock.take() {
hnd.sync_all().map_err(|e| {
HostError::new(
HostErrorType::FileNotAccessible,
Some(e),
self.tempfile.path(),
)
})?;
}
}
Ok(())
}
fn write_hnd(&mut self) -> io::Result<Arc<Mutex<Option<File>>>> {
{
let mut lock = self.handle.lock().unwrap();
if lock.is_none() {
let hnd = File::create(self.tempfile.path())?;
lock.replace(hnd);
}
}
Ok(self.handle.clone())
}
fn read_hnd(&mut self) -> io::Result<Arc<Mutex<Option<File>>>> {
{
let mut lock = self.handle.lock().unwrap();
if lock.is_none() {
let hnd = File::open(self.tempfile.path())?;
lock.replace(hnd);
}
}
Ok(self.handle.clone())
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_should_write_and_read_file() {
let mut file = TempMappedFile::new().unwrap();
file.write_all(b"Hello, World!").unwrap();
file.sync().unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
assert_eq!(buf, b"Hello, World!");
}
}

View File

@@ -1,5 +1,13 @@
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION"); mod activity_manager;
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); mod cli;
mod config;
mod explorer;
mod filetransfer;
mod host;
mod support;
mod system;
mod ui;
mod utils;
// Crates // Crates
#[macro_use] #[macro_use]
@@ -13,28 +21,27 @@ extern crate log;
#[macro_use] #[macro_use]
extern crate magic_crypt; extern crate magic_crypt;
// External libs
use std::env; use std::env;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::time::Duration; use std::time::Duration;
// Include use self::activity_manager::{ActivityManager, NextActivity};
mod activity_manager; use self::cli::{Args, ArgsSubcommands, RemoteArgs, RunOpts, Task};
mod cli_opts; use self::system::logging::{self, LogLevel};
mod config;
mod explorer;
mod filetransfer;
mod host;
mod support;
mod system;
mod ui;
mod utils;
// namespaces const APP_NAME: &str = env!("CARGO_PKG_NAME");
use activity_manager::{ActivityManager, NextActivity}; const APP_BUILD_DATE: &str = env!("VERGEN_BUILD_TIMESTAMP");
use cli_opts::{Args, ArgsSubcommands, BookmarkParams, HostParams, Remote, RunOpts, Task}; const APP_GIT_BRANCH: &str = env!("VERGEN_GIT_BRANCH");
use filetransfer::FileTransferParams; const APP_GIT_HASH: &str = env!("VERGEN_GIT_SHA");
use system::logging::{self, LogLevel}; const EXIT_CODE_SUCCESS: i32 = 0;
const EXIT_CODE_ERROR: i32 = 1;
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
#[inline]
fn git_hash() -> &'static str {
APP_GIT_HASH[0..8].as_ref()
}
fn main() { fn main() {
let args: Args = argh::from_env(); let args: Args = argh::from_env();
@@ -50,7 +57,10 @@ fn main() {
if let Err(err) = logging::init(run_opts.log_level) { if let Err(err) = logging::init(run_opts.log_level) {
eprintln!("Failed to initialize logging: {err}"); eprintln!("Failed to initialize logging: {err}");
} }
info!("termscp {} started!", TERMSCP_VERSION); info!(
"{APP_NAME} v{TERMSCP_VERSION} ({APP_GIT_BRANCH}, {git_hash}, {APP_BUILD_DATE}) - Developed by {TERMSCP_AUTHORS}",
git_hash = git_hash()
);
// Run // Run
info!("Starting activity manager..."); info!("Starting activity manager...");
let rc = run(run_opts); let rc = run(run_opts);
@@ -72,7 +82,8 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
// Version // Version
if args.version { if args.version {
return Err(format!( return Err(format!(
"termscp - {TERMSCP_VERSION} - Developed by {TERMSCP_AUTHORS}", "{APP_NAME} v{TERMSCP_VERSION} ({APP_GIT_BRANCH}, {git_hash}, {APP_BUILD_DATE}) - Developed by {TERMSCP_AUTHORS}",
git_hash = git_hash()
)); ));
} }
// Logging // Logging
@@ -84,22 +95,24 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
// Match ticks // Match ticks
run_opts.ticks = Duration::from_millis(args.ticks); run_opts.ticks = Duration::from_millis(args.ticks);
// Remote argument // Remote argument
match parse_address_arg(&args) { match RemoteArgs::try_from(&args) {
Err(err) => return Err(err), Err(err) => return Err(err),
Ok(Remote::None) => {}
Ok(remote) => { Ok(remote) => {
// Set params // Set params
run_opts.remote = remote; run_opts.remote = remote;
// In this case the first activity will be FileTransfer
run_opts.task = Task::Activity(NextActivity::FileTransfer);
} }
} }
// set activity based on remote state
run_opts.task = if run_opts.remote.remote.is_none() {
Task::Activity(NextActivity::Authentication)
} else {
Task::Activity(NextActivity::FileTransfer)
};
// Local directory // Local directory
if let Some(localdir) = args.positional.get(1) { if let Some(localdir) = run_opts.remote.local_dir.as_deref() {
// Change working directory if local dir is set if let Err(err) = env::set_current_dir(localdir) {
let localdir: PathBuf = PathBuf::from(localdir);
if let Err(err) = env::set_current_dir(localdir.as_path()) {
return Err(format!("Bad working directory argument: {err}")); return Err(format!("Bad working directory argument: {err}"));
} }
} }
@@ -111,29 +124,6 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
Ok(run_opts) Ok(run_opts)
} }
/// Parse address argument from cli args
fn parse_address_arg(args: &Args) -> Result<Remote, String> {
if let Some(remote) = args.positional.first() {
if args.address_as_bookmark {
Ok(Remote::Bookmark(BookmarkParams::new(
remote,
args.password.as_ref(),
)))
} else {
// Parse address
parse_remote_address(remote.as_str())
.map(|x| Remote::Host(HostParams::new(x, args.password.as_deref())))
}
} else {
Ok(Remote::None)
}
}
/// Parse remote address
fn parse_remote_address(remote: &str) -> Result<FileTransferParams, String> {
utils::parser::parse_remote_opt(remote).map_err(|e| format!("Bad address option: {e}"))
}
/// Run task and return rc /// Run task and return rc
fn run(run_opts: RunOpts) -> i32 { fn run(run_opts: RunOpts) -> i32 {
match run_opts.task { match run_opts.task {
@@ -147,11 +137,11 @@ fn run_import_theme(theme: &Path) -> i32 {
match support::import_theme(theme) { match support::import_theme(theme) {
Ok(_) => { Ok(_) => {
println!("Theme has been successfully imported!"); println!("Theme has been successfully imported!");
0 EXIT_CODE_ERROR
} }
Err(err) => { Err(err) => {
eprintln!("{err}"); eprintln!("{err}");
1 EXIT_CODE_ERROR
} }
} }
} }
@@ -160,41 +150,32 @@ fn run_install_update() -> i32 {
match support::install_update() { match support::install_update() {
Ok(msg) => { Ok(msg) => {
println!("{msg}"); println!("{msg}");
0 EXIT_CODE_SUCCESS
} }
Err(err) => { Err(err) => {
eprintln!("Could not install update: {err}"); eprintln!("Could not install update: {err}");
1 EXIT_CODE_ERROR
} }
} }
} }
fn run_activity(activity: NextActivity, ticks: Duration, remote: Remote) -> i32 { fn run_activity(activity: NextActivity, ticks: Duration, remote_args: RemoteArgs) -> i32 {
// Create activity manager (and context too) // Create activity manager (and context too)
let mut manager: ActivityManager = match ActivityManager::new(ticks) { let mut manager: ActivityManager = match ActivityManager::new(ticks) {
Ok(m) => m, Ok(m) => m,
Err(err) => { Err(err) => {
eprintln!("Could not start activity manager: {err}"); eprintln!("Could not start activity manager: {err}");
return 1; return EXIT_CODE_ERROR;
} }
}; };
// Set file transfer params if set // Set file transfer params if set
match remote { if let Err(err) = manager.configure_remote_args(remote_args) {
Remote::Bookmark(BookmarkParams { name, password }) => { eprintln!("{err}");
if let Err(err) = manager.resolve_bookmark_name(&name, password.as_deref()) { return EXIT_CODE_ERROR;
eprintln!("{err}");
return 1;
}
}
Remote::Host(HostParams { params, password }) => {
if let Err(err) = manager.set_filetransfer_params(params, password.as_deref()) {
eprintln!("{err}");
return 1;
}
}
Remote::None => {}
} }
manager.run(activity); manager.run(activity);
0 EXIT_CODE_SUCCESS
} }

View File

@@ -85,6 +85,7 @@ impl Update {
/// In case received version is newer than current one, version as Some is returned; otherwise None /// In case received version is newer than current one, version as Some is returned; otherwise None
fn check_version(r: Release) -> Option<Release> { fn check_version(r: Release) -> Option<Release> {
debug!("got version from GitHub: {}", r.version);
match parse_semver(r.version.as_str()) { match parse_semver(r.version.as_str()) {
Some(new_version) => { Some(new_version) => {
// Check if version is different // Check if version is different

View File

@@ -25,7 +25,11 @@ pub fn init(level: LogLevel) -> Result<(), String> {
let file = open_file(log_file_path.as_path(), true, true, false) let file = open_file(log_file_path.as_path(), true, true, false)
.map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?; .map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?;
// Prepare log config // Prepare log config
let config = ConfigBuilder::new().set_time_format_rfc3339().build(); let config = ConfigBuilder::new()
.set_time_format_rfc3339()
.add_filter_allow_str("termscp")
.add_filter_allow_str("remotefs")
.build();
// Make logger // Make logger
WriteLogger::init(level, config, file).map_err(|e| format!("Failed to initialize logger: {e}")) WriteLogger::init(level, config, file).map_err(|e| format!("Failed to initialize logger: {e}"))
} }

View File

@@ -143,7 +143,7 @@ impl ThemeProvider {
mod test { mod test {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use tempfile::TempDir; use tempfile::TempDir;
use tuirealm::tui::style::Color; use tuirealm::ratatui::style::Color;
use super::*; use super::*;

View File

@@ -136,7 +136,7 @@ impl FileToRemove {
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct FileUpdate { pub struct FileUpdate {
/// Path to file which has changed /// Path to file which has changed
local: PathBuf, host_bridge: PathBuf,
/// Path to remote file to update /// Path to remote file to update
remote: PathBuf, remote: PathBuf,
} }
@@ -152,13 +152,13 @@ impl FileUpdate {
fn new(changed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self { fn new(changed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self {
Self { Self {
remote: remote_relative_path(&changed_path, local_watched_path, remote_synched_path), remote: remote_relative_path(&changed_path, local_watched_path, remote_synched_path),
local: changed_path, host_bridge: changed_path,
} }
} }
/// Get path to local file to sync /// Get path to local file to sync
pub fn local(&self) -> &Path { pub fn host_bridge(&self) -> &Path {
self.local.as_path() self.host_bridge.as_path()
} }
/// Get path to remote file to sync /// Get path to remote file to sync
@@ -288,7 +288,7 @@ mod test {
Path::new("/home/foo/bar.txt"), Path::new("/home/foo/bar.txt"),
); );
if let FsChange::Update(change) = change { if let FsChange::Update(change) = change {
assert_eq!(change.local(), Path::new("/tmp/bar.txt"),); assert_eq!(change.host_bridge(), Path::new("/tmp/bar.txt"),);
assert_eq!(change.remote(), Path::new("/home/foo/bar.txt")); assert_eq!(change.remote(), Path::new("/home/foo/bar.txt"));
} else { } else {
panic!("not an update"); panic!("not an update");
@@ -303,7 +303,7 @@ mod test {
Path::new("/home/foo/temp"), Path::new("/home/foo/temp"),
); );
if let FsChange::Update(change) = change { if let FsChange::Update(change) = change {
assert_eq!(change.local(), Path::new("/tmp/abc/foo.txt"),); assert_eq!(change.host_bridge(), Path::new("/tmp/abc/foo.txt"),);
assert_eq!(change.remote(), Path::new("/home/foo/temp/abc/foo.txt")); assert_eq!(change.remote(), Path::new("/home/foo/temp/abc/foo.txt"));
} else { } else {
panic!("not an update"); panic!("not an update");

View File

@@ -385,7 +385,7 @@ mod test {
/* /*
#[test] #[test]
#[cfg(unix)] #[cfg(posix)]
fn should_poll_file_moved() { fn should_poll_file_moved() {
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap(); let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
let tempdir = TempDir::new().unwrap(); let tempdir = TempDir::new().unwrap();

View File

@@ -3,11 +3,12 @@
//! `auth_activity` is the module which implements the authentication activity //! `auth_activity` is the module which implements the authentication activity
// Locals // Locals
use super::{AuthActivity, FileTransferParams}; use super::{AuthActivity, FileTransferParams, FormTab, HostBridgeProtocol};
use crate::filetransfer::params::{ use crate::filetransfer::params::{
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams, AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
WebDAVProtocolParams, WebDAVProtocolParams,
}; };
use crate::filetransfer::HostBridgeParams;
impl AuthActivity { impl AuthActivity {
/// Delete bookmark /// Delete bookmark
@@ -26,27 +27,49 @@ impl AuthActivity {
} }
/// Load selected bookmark (at index) to input fields /// Load selected bookmark (at index) to input fields
pub(super) fn load_bookmark(&mut self, idx: usize) { pub(super) fn load_bookmark(&mut self, form_tab: FormTab, idx: usize) {
if let Some(bookmarks_cli) = self.bookmarks_client() { if let Some(bookmarks_cli) = self.bookmarks_client() {
// Iterate over bookmarks // Iterate over bookmarks
if let Some(key) = self.bookmarks_list.get(idx) { if let Some(key) = self.bookmarks_list.get(idx) {
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) { if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
// Load parameters into components // Load parameters into components
self.load_bookmark_into_gui(bookmark); match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
}
} }
} }
} }
} }
/// Save current input fields as a bookmark /// Save current input fields as a bookmark
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) { pub(super) fn save_bookmark(&mut self, form_tab: FormTab, name: String, save_password: bool) {
let params = match self.collect_host_params() { let params = match form_tab {
Ok(p) => p, FormTab::Remote => match self.collect_remote_host_params() {
Err(e) => { Ok(p) => p,
self.mount_error(e); Err(e) => {
return; self.mount_error(e);
} return;
}
},
FormTab::HostBridge => match self.collect_host_bridge_params() {
Ok(HostBridgeParams::Remote(protocol, params)) => FileTransferParams {
protocol,
params,
remote_path: None,
local_path: None,
},
Ok(HostBridgeParams::Localhost(_)) => {
self.mount_error("You cannot save a localhost bookmark");
return;
}
Err(e) => {
self.mount_error(e);
return;
}
},
}; };
if let Some(bookmarks_cli) = self.bookmarks_client_mut() { if let Some(bookmarks_cli) = self.bookmarks_client_mut() {
bookmarks_cli.add_bookmark(name.clone(), params, save_password); bookmarks_cli.add_bookmark(name.clone(), params, save_password);
// Save bookmarks // Save bookmarks
@@ -73,13 +96,16 @@ impl AuthActivity {
} }
/// Load selected recent (at index) to input fields /// Load selected recent (at index) to input fields
pub(super) fn load_recent(&mut self, idx: usize) { pub(super) fn load_recent(&mut self, form_tab: FormTab, idx: usize) {
if let Some(client) = self.bookmarks_client() { if let Some(client) = self.bookmarks_client() {
// Iterate over bookmarks // Iterate over bookmarks
if let Some(key) = self.recents_list.get(idx) { if let Some(key) = self.recents_list.get(idx) {
if let Some(bookmark) = client.get_recent(key) { if let Some(bookmark) = client.get_recent(key) {
// Load parameters // Load parameters
self.load_bookmark_into_gui(bookmark); match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
}
} }
} }
} }
@@ -87,7 +113,7 @@ impl AuthActivity {
/// Save current input fields as a "recent" /// Save current input fields as a "recent"
pub(super) fn save_recent(&mut self) { pub(super) fn save_recent(&mut self) {
let params = match self.collect_host_params() { let params = match self.collect_remote_host_params() {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
self.mount_error(e); self.mount_error(e);
@@ -147,73 +173,125 @@ impl AuthActivity {
} }
/// Load bookmark data into the gui components /// Load bookmark data into the gui components
fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) { fn load_host_bridge_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
// Load parameters into components // Load parameters into components
self.protocol = bookmark.protocol; self.host_bridge_protocol = HostBridgeProtocol::Remote(bookmark.protocol);
self.mount_protocol(bookmark.protocol); self.mount_host_bridge_protocol(self.host_bridge_protocol);
self.mount_remote_directory( self.mount_remote_directory(
FormTab::HostBridge,
bookmark bookmark
.remote_path .remote_path
.map(|x| x.to_string_lossy().to_string()) .map(|x| x.to_string_lossy().to_string())
.unwrap_or_default(), .unwrap_or_default(),
); );
self.mount_local_directory( self.mount_local_directory(
FormTab::HostBridge,
bookmark bookmark
.local_path .local_path
.map(|x| x.to_string_lossy().to_string()) .map(|x| x.to_string_lossy().to_string())
.unwrap_or_default(), .unwrap_or_default(),
); );
match bookmark.params { match bookmark.params {
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params), ProtocolParams::AwsS3(params) => {
ProtocolParams::Kube(params) => self.load_bookmark_kube_into_gui(params), self.load_bookmark_s3_into_gui(FormTab::HostBridge, params)
}
ProtocolParams::Kube(params) => {
self.load_bookmark_kube_into_gui(FormTab::HostBridge, params)
}
ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params), ProtocolParams::Generic(params) => {
ProtocolParams::Smb(params) => self.load_bookmark_smb_into_gui(params), self.load_bookmark_generic_into_gui(FormTab::HostBridge, params)
ProtocolParams::WebDAV(params) => self.load_bookmark_webdav_into_gui(params), }
ProtocolParams::Smb(params) => {
self.load_bookmark_smb_into_gui(FormTab::HostBridge, params)
}
ProtocolParams::WebDAV(params) => {
self.load_bookmark_webdav_into_gui(FormTab::HostBridge, params)
}
} }
} }
fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) { /// Load bookmark data into the gui components
self.mount_address(params.address.as_str()); fn load_remote_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
self.mount_port(params.port); // Load parameters into components
self.mount_username(params.username.as_deref().unwrap_or("")); self.remote_protocol = bookmark.protocol;
self.mount_password(params.password.as_deref().unwrap_or("")); self.mount_remote_protocol(bookmark.protocol);
self.mount_remote_directory(
FormTab::Remote,
bookmark
.remote_path
.map(|x| x.to_string_lossy().to_string())
.unwrap_or_default(),
);
self.mount_local_directory(
FormTab::Remote,
bookmark
.local_path
.map(|x| x.to_string_lossy().to_string())
.unwrap_or_default(),
);
match bookmark.params {
ProtocolParams::AwsS3(params) => {
self.load_bookmark_s3_into_gui(FormTab::Remote, params)
}
ProtocolParams::Kube(params) => {
self.load_bookmark_kube_into_gui(FormTab::Remote, params)
}
ProtocolParams::Generic(params) => {
self.load_bookmark_generic_into_gui(FormTab::Remote, params)
}
ProtocolParams::Smb(params) => self.load_bookmark_smb_into_gui(FormTab::Remote, params),
ProtocolParams::WebDAV(params) => {
self.load_bookmark_webdav_into_gui(FormTab::Remote, params)
}
}
} }
fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) { fn load_bookmark_generic_into_gui(&mut self, form_tab: FormTab, params: GenericProtocolParams) {
self.mount_s3_bucket(params.bucket_name.as_str()); self.mount_address(form_tab, params.address.as_str());
self.mount_s3_region(params.region.as_deref().unwrap_or("")); self.mount_port(form_tab, params.port);
self.mount_s3_endpoint(params.endpoint.as_deref().unwrap_or("")); self.mount_username(form_tab, params.username.as_deref().unwrap_or(""));
self.mount_s3_profile(params.profile.as_deref().unwrap_or("")); self.mount_password(form_tab, params.password.as_deref().unwrap_or(""));
self.mount_s3_access_key(params.access_key.as_deref().unwrap_or(""));
self.mount_s3_secret_access_key(params.secret_access_key.as_deref().unwrap_or(""));
self.mount_s3_security_token(params.security_token.as_deref().unwrap_or(""));
self.mount_s3_session_token(params.session_token.as_deref().unwrap_or(""));
self.mount_s3_new_path_style(params.new_path_style);
} }
fn load_bookmark_kube_into_gui(&mut self, params: KubeProtocolParams) { fn load_bookmark_s3_into_gui(&mut self, form_tab: FormTab, params: AwsS3Params) {
self.mount_kube_cluster_url(params.cluster_url.as_deref().unwrap_or("")); self.mount_s3_bucket(form_tab, params.bucket_name.as_str());
self.mount_kube_namespace(params.namespace.as_deref().unwrap_or("")); self.mount_s3_region(form_tab, params.region.as_deref().unwrap_or(""));
self.mount_kube_client_cert(params.client_cert.as_deref().unwrap_or("")); self.mount_s3_endpoint(form_tab, params.endpoint.as_deref().unwrap_or(""));
self.mount_kube_client_key(params.client_key.as_deref().unwrap_or("")); self.mount_s3_profile(form_tab, params.profile.as_deref().unwrap_or(""));
self.mount_kube_username(params.username.as_deref().unwrap_or("")); self.mount_s3_access_key(form_tab, params.access_key.as_deref().unwrap_or(""));
self.mount_s3_secret_access_key(
form_tab,
params.secret_access_key.as_deref().unwrap_or(""),
);
self.mount_s3_security_token(form_tab, params.security_token.as_deref().unwrap_or(""));
self.mount_s3_session_token(form_tab, params.session_token.as_deref().unwrap_or(""));
self.mount_s3_new_path_style(form_tab, params.new_path_style);
} }
fn load_bookmark_smb_into_gui(&mut self, params: SmbParams) { fn load_bookmark_kube_into_gui(&mut self, form_tab: FormTab, params: KubeProtocolParams) {
self.mount_address(params.address.as_str()); self.mount_kube_cluster_url(form_tab, params.cluster_url.as_deref().unwrap_or(""));
#[cfg(unix)] self.mount_kube_namespace(form_tab, params.namespace.as_deref().unwrap_or(""));
self.mount_port(params.port); self.mount_kube_client_cert(form_tab, params.client_cert.as_deref().unwrap_or(""));
self.mount_username(params.username.as_deref().unwrap_or("")); self.mount_kube_client_key(form_tab, params.client_key.as_deref().unwrap_or(""));
self.mount_password(params.password.as_deref().unwrap_or("")); self.mount_kube_username(form_tab, params.username.as_deref().unwrap_or(""));
self.mount_smb_share(&params.share);
#[cfg(unix)]
self.mount_smb_workgroup(params.workgroup.as_deref().unwrap_or(""));
} }
fn load_bookmark_webdav_into_gui(&mut self, params: WebDAVProtocolParams) { fn load_bookmark_smb_into_gui(&mut self, form_tab: FormTab, params: SmbParams) {
self.mount_webdav_uri(&params.uri); self.mount_address(form_tab, params.address.as_str());
self.mount_username(&params.username); #[cfg(posix)]
self.mount_password(&params.password); self.mount_port(form_tab, params.port);
self.mount_username(form_tab, params.username.as_deref().unwrap_or(""));
self.mount_password(form_tab, params.password.as_deref().unwrap_or(""));
self.mount_smb_share(form_tab, &params.share);
#[cfg(posix)]
self.mount_smb_workgroup(form_tab, params.workgroup.as_deref().unwrap_or(""));
}
fn load_bookmark_webdav_into_gui(&mut self, form_tab: FormTab, params: WebDAVProtocolParams) {
self.mount_webdav_uri(form_tab, &params.uri);
self.mount_username(form_tab, &params.username);
self.mount_password(form_tab, &params.password);
} }
} }

View File

@@ -9,6 +9,7 @@ use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputT
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
use super::{FormMsg, Msg, UiMsg}; use super::{FormMsg, Msg, UiMsg};
use crate::ui::activities::auth::FormTab;
// -- bookmark list // -- bookmark list
@@ -323,10 +324,11 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
#[derive(MockComponent)] #[derive(MockComponent)]
pub struct BookmarkSavePassword { pub struct BookmarkSavePassword {
component: Radio, component: Radio,
form_tab: FormTab,
} }
impl BookmarkSavePassword { impl BookmarkSavePassword {
pub fn new(color: Color) -> Self { pub fn new(form_tab: FormTab, color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.borders( .borders(
@@ -340,6 +342,7 @@ impl BookmarkSavePassword {
.rewind(true) .rewind(true)
.foreground(color) .foreground(color)
.title("Save secrets?", Alignment::Center), .title("Save secrets?", Alignment::Center),
form_tab,
} }
} }
} }
@@ -364,7 +367,7 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => Some(Msg::Form(FormMsg::SaveBookmark)), }) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
Some(Msg::Ui(UiMsg::SaveBookmarkPasswordBlur)) Some(Msg::Ui(UiMsg::SaveBookmarkPasswordBlur))
} }
@@ -378,10 +381,11 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
#[derive(MockComponent)] #[derive(MockComponent)]
pub struct BookmarkName { pub struct BookmarkName {
component: Input, component: Input,
form_tab: FormTab,
} }
impl BookmarkName { impl BookmarkName {
pub fn new(color: Color) -> Self { pub fn new(form_tab: FormTab, color: Color) -> Self {
Self { Self {
component: Input::default() component: Input::default()
.borders( .borders(
@@ -393,6 +397,7 @@ impl BookmarkName {
.foreground(color) .foreground(color)
.title("Bookmark name", Alignment::Left) .title("Bookmark name", Alignment::Left)
.input_type(InputType::Text), .input_type(InputType::Text),
form_tab,
} }
} }
} }
@@ -447,7 +452,7 @@ impl Component<Msg, NoUserEvent> for BookmarkName {
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => Some(Msg::Form(FormMsg::SaveBookmark)), }) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))),
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Down, .. code: Key::Down, ..
}) => Some(Msg::Ui(UiMsg::BookmarkNameBlur)), }) => Some(Msg::Ui(UiMsg::BookmarkNameBlur)),

File diff suppressed because it is too large Load Diff

View File

@@ -13,14 +13,15 @@ pub use bookmarks::{
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup, BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
RecentsList, RecentsList,
}; };
#[cfg(unix)] #[cfg(posix)]
pub use form::InputSmbWorkgroup; pub use form::InputSmbWorkgroup;
pub use form::{ pub use form::{
InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeNamespace, HostBridgeProtocolRadio, InputAddress, InputKubeClientCert, InputKubeClientKey,
InputKubeUsername, InputLocalDirectory, InputPassword, InputPort, InputRemoteDirectory, InputKubeClusterUrl, InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword,
InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile, InputS3Region, InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint,
InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputSmbShare, InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
InputUsername, InputWebDAVUri, ProtocolRadio, RadioS3NewPathStyle, InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle,
RemoteProtocolRadio,
}; };
pub use popup::{ pub use popup::{
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup, ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,

View File

@@ -100,6 +100,8 @@ impl HelpFooter {
TextSpan::from(" Change field "), TextSpan::from(" Change field "),
TextSpan::from("<TAB>").bold().fg(key_color), TextSpan::from("<TAB>").bold().fg(key_color),
TextSpan::from(" Switch tab "), TextSpan::from(" Switch tab "),
TextSpan::from("<BACKTAB>").bold().fg(key_color),
TextSpan::from(" Switch form "),
TextSpan::from("<ENTER>").bold().fg(key_color), TextSpan::from("<ENTER>").bold().fg(key_color),
TextSpan::from(" Submit form "), TextSpan::from(" Submit form "),
TextSpan::from("<F10|ESC>").bold().fg(key_color), TextSpan::from("<F10|ESC>").bold().fg(key_color),

View File

@@ -2,8 +2,11 @@
//! //!
//! `auth_activity` is the module which implements the authentication activity //! `auth_activity` is the module which implements the authentication activity
use super::{AuthActivity, FileTransferParams, FileTransferProtocol}; use std::env;
use super::{AuthActivity, FileTransferParams, FileTransferProtocol, FormTab, HostBridgeProtocol};
use crate::filetransfer::params::ProtocolParams; use crate::filetransfer::params::ProtocolParams;
use crate::filetransfer::HostBridgeParams;
use crate::system::auto_update::{Release, Update, UpdateStatus}; use crate::system::auto_update::{Release, Update, UpdateStatus};
use crate::system::notifications::Notification; use crate::system::notifications::Notification;
@@ -36,24 +39,64 @@ impl AuthActivity {
} }
/// Collect host params as `FileTransferParams` /// Collect host params as `FileTransferParams`
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> { pub(super) fn collect_host_bridge_params(&self) -> Result<HostBridgeParams, &'static str> {
match self.protocol { match self.host_bridge_protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(), HostBridgeProtocol::Localhost => self.collect_localhost_host_params(),
FileTransferProtocol::Kube => self.collect_kube_host_params(), HostBridgeProtocol::Remote(remote) => {
FileTransferProtocol::Smb => self.collect_smb_host_params(), let transfer_params = match remote {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(FormTab::HostBridge),
FileTransferProtocol::Kube => {
self.collect_kube_host_params(FormTab::HostBridge)
}
FileTransferProtocol::Smb => self.collect_smb_host_params(FormTab::HostBridge),
FileTransferProtocol::Ftp(_)
| FileTransferProtocol::Scp
| FileTransferProtocol::Sftp => {
self.collect_generic_host_params(remote, FormTab::HostBridge)
}
FileTransferProtocol::WebDAV => {
self.collect_webdav_host_params(FormTab::HostBridge)
}
}?;
Ok(HostBridgeParams::Remote(
transfer_params.protocol,
transfer_params.params,
))
}
}
}
/// Collect host params as `FileTransferParams`
pub(super) fn collect_remote_host_params(&self) -> Result<FileTransferParams, &'static str> {
match self.remote_protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(FormTab::Remote),
FileTransferProtocol::Kube => self.collect_kube_host_params(FormTab::Remote),
FileTransferProtocol::Smb => self.collect_smb_host_params(FormTab::Remote),
FileTransferProtocol::Ftp(_) FileTransferProtocol::Ftp(_)
| FileTransferProtocol::Scp | FileTransferProtocol::Scp
| FileTransferProtocol::Sftp => self.collect_generic_host_params(self.protocol), | FileTransferProtocol::Sftp => {
FileTransferProtocol::WebDAV => self.collect_webdav_host_params(), self.collect_generic_host_params(self.remote_protocol, FormTab::Remote)
}
FileTransferProtocol::WebDAV => self.collect_webdav_host_params(FormTab::Remote),
} }
} }
fn collect_localhost_host_params(&self) -> Result<HostBridgeParams, &'static str> {
let path = self
.get_input_local_directory(FormTab::HostBridge)
.unwrap_or_else(|| env::current_dir().unwrap_or_default());
Ok(HostBridgeParams::Localhost(path))
}
/// Get input values from fields or return an error if fields are invalid to work as generic /// Get input values from fields or return an error if fields are invalid to work as generic
pub(super) fn collect_generic_host_params( pub(super) fn collect_generic_host_params(
&self, &self,
protocol: FileTransferProtocol, protocol: FileTransferProtocol,
form_tab: FormTab,
) -> Result<FileTransferParams, &'static str> { ) -> Result<FileTransferParams, &'static str> {
let params = self.get_generic_params_input(); let params = self.get_generic_params_input(form_tab);
if params.address.is_empty() { if params.address.is_empty() {
return Err("Invalid host"); return Err("Invalid host");
} }
@@ -63,43 +106,52 @@ impl AuthActivity {
Ok(FileTransferParams { Ok(FileTransferParams {
protocol, protocol,
params: ProtocolParams::Generic(params), params: ProtocolParams::Generic(params),
local_path: self.get_input_local_directory(), local_path: self.get_input_local_directory(form_tab),
remote_path: self.get_input_remote_directory(), remote_path: self.get_input_remote_directory(form_tab),
}) })
} }
/// Get input values from fields or return an error if fields are invalid to work as aws s3 /// Get input values from fields or return an error if fields are invalid to work as aws s3
pub(super) fn collect_s3_host_params(&self) -> Result<FileTransferParams, &'static str> { pub(super) fn collect_s3_host_params(
let params = self.get_s3_params_input(); &self,
form_tab: FormTab,
) -> Result<FileTransferParams, &'static str> {
let params = self.get_s3_params_input(form_tab);
if params.bucket_name.is_empty() { if params.bucket_name.is_empty() {
return Err("Invalid bucket"); return Err("Invalid bucket");
} }
Ok(FileTransferParams { Ok(FileTransferParams {
protocol: FileTransferProtocol::AwsS3, protocol: FileTransferProtocol::AwsS3,
params: ProtocolParams::AwsS3(params), params: ProtocolParams::AwsS3(params),
local_path: self.get_input_local_directory(), local_path: self.get_input_local_directory(form_tab),
remote_path: self.get_input_remote_directory(), remote_path: self.get_input_remote_directory(form_tab),
}) })
} }
/// Get input values from fields or return an error if fields are invalid to work as aws s3 /// Get input values from fields or return an error if fields are invalid to work as aws s3
pub(super) fn collect_kube_host_params(&self) -> Result<FileTransferParams, &'static str> { pub(super) fn collect_kube_host_params(
let params = self.get_kube_params_input(); &self,
form_tab: FormTab,
) -> Result<FileTransferParams, &'static str> {
let params = self.get_kube_params_input(form_tab);
Ok(FileTransferParams { Ok(FileTransferParams {
protocol: FileTransferProtocol::Kube, protocol: FileTransferProtocol::Kube,
params: ProtocolParams::Kube(params), params: ProtocolParams::Kube(params),
local_path: self.get_input_local_directory(), local_path: self.get_input_local_directory(form_tab),
remote_path: self.get_input_remote_directory(), remote_path: self.get_input_remote_directory(form_tab),
}) })
} }
pub(super) fn collect_smb_host_params(&self) -> Result<FileTransferParams, &'static str> { pub(super) fn collect_smb_host_params(
let params = self.get_smb_params_input(); &self,
form_tab: FormTab,
) -> Result<FileTransferParams, &'static str> {
let params = self.get_smb_params_input(form_tab);
if params.address.is_empty() { if params.address.is_empty() {
return Err("Invalid address"); return Err("Invalid address");
} }
#[cfg(unix)] #[cfg(posix)]
if params.port == 0 { if params.port == 0 {
return Err("Invalid port"); return Err("Invalid port");
} }
@@ -109,21 +161,24 @@ impl AuthActivity {
Ok(FileTransferParams { Ok(FileTransferParams {
protocol: FileTransferProtocol::Smb, protocol: FileTransferProtocol::Smb,
params: ProtocolParams::Smb(params), params: ProtocolParams::Smb(params),
local_path: self.get_input_local_directory(), local_path: self.get_input_local_directory(form_tab),
remote_path: self.get_input_remote_directory(), remote_path: self.get_input_remote_directory(form_tab),
}) })
} }
pub(super) fn collect_webdav_host_params(&self) -> Result<FileTransferParams, &'static str> { pub(super) fn collect_webdav_host_params(
let params = self.get_webdav_params_input(); &self,
form_tab: FormTab,
) -> Result<FileTransferParams, &'static str> {
let params = self.get_webdav_params_input(form_tab);
if params.uri.is_empty() { if params.uri.is_empty() {
return Err("Invalid URI"); return Err("Invalid URI");
} }
Ok(FileTransferParams { Ok(FileTransferParams {
protocol: FileTransferProtocol::WebDAV, protocol: FileTransferProtocol::WebDAV,
params: ProtocolParams::WebDAV(params), params: ProtocolParams::WebDAV(params),
local_path: self.get_input_local_directory(), local_path: self.get_input_local_directory(form_tab),
remote_path: self.get_input_remote_directory(), remote_path: self.get_input_remote_directory(form_tab),
}) })
} }

View File

@@ -17,26 +17,36 @@ use tuirealm::application::PollStrategy;
use tuirealm::listener::EventListenerCfg; use tuirealm::listener::EventListenerCfg;
use tuirealm::{Application, NoUserEvent, Update}; use tuirealm::{Application, NoUserEvent, Update};
use super::{Activity, Context, ExitReason}; use super::{Activity, Context, ExitReason, CROSSTERM_MAX_POLL};
use crate::config::themes::Theme; use crate::config::themes::Theme;
use crate::filetransfer::{FileTransferParams, FileTransferProtocol}; use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
use crate::system::bookmarks_client::BookmarksClient; use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
// radio // host bridge protocol radio
const RADIO_PROTOCOL_SFTP: usize = 0; const HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST: usize = 0;
const RADIO_PROTOCOL_SCP: usize = 1; const HOST_BRIDGE_RADIO_PROTOCOL_SFTP: usize = 1;
const RADIO_PROTOCOL_FTP: usize = 2; const HOST_BRIDGE_RADIO_PROTOCOL_SCP: usize = 2;
const RADIO_PROTOCOL_FTPS: usize = 3; const HOST_BRIDGE_RADIO_PROTOCOL_FTP: usize = 3;
const RADIO_PROTOCOL_S3: usize = 4; const HOST_BRIDGE_RADIO_PROTOCOL_FTPS: usize = 4;
const RADIO_PROTOCOL_KUBE: usize = 5; const HOST_BRIDGE_RADIO_PROTOCOL_S3: usize = 5;
const RADIO_PROTOCOL_WEBDAV: usize = 6; const HOST_BRIDGE_RADIO_PROTOCOL_KUBE: usize = 6;
const RADIO_PROTOCOL_SMB: usize = 7; const HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV: usize = 7;
const HOST_BRIDGE_RADIO_PROTOCOL_SMB: usize = 8; // Keep as last
// remote protocol radio
const REMOTE_RADIO_PROTOCOL_SFTP: usize = 0;
const REMOTE_RADIO_PROTOCOL_SCP: usize = 1;
const REMOTE_RADIO_PROTOCOL_FTP: usize = 2;
const REMOTE_RADIO_PROTOCOL_FTPS: usize = 3;
const REMOTE_RADIO_PROTOCOL_S3: usize = 4;
const REMOTE_RADIO_PROTOCOL_KUBE: usize = 5;
const REMOTE_RADIO_PROTOCOL_WEBDAV: usize = 6;
const REMOTE_RADIO_PROTOCOL_SMB: usize = 7; // Keep as last
// -- components // -- components
#[derive(Debug, Eq, PartialEq, Clone, Hash)] #[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Id { pub enum Id {
Address,
BookmarkName, BookmarkName,
BookmarkSavePassword, BookmarkSavePassword,
BookmarksList, BookmarksList,
@@ -45,22 +55,33 @@ pub enum Id {
ErrorPopup, ErrorPopup,
GlobalListener, GlobalListener,
HelpFooter, HelpFooter,
HostBridge(AuthFormId),
InfoPopup, InfoPopup,
InstallUpdatePopup, InstallUpdatePopup,
Keybindings, Keybindings,
NewVersionChangelog,
NewVersionDisclaimer,
QuitPopup,
RecentsList,
Remote(AuthFormId),
Subtitle,
Title,
WaitPopup,
WindowSizeError,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum AuthFormId {
Address,
KubeNamespace, KubeNamespace,
KubeClusterUrl, KubeClusterUrl,
KubeUsername, KubeUsername,
KubeClientCert, KubeClientCert,
KubeClientKey, KubeClientKey,
LocalDirectory, LocalDirectory,
NewVersionChangelog,
NewVersionDisclaimer,
Password, Password,
Port, Port,
Protocol, Protocol,
QuitPopup,
RecentsList,
RemoteDirectory, RemoteDirectory,
S3AccessKey, S3AccessKey,
S3Bucket, S3Bucket,
@@ -72,25 +93,21 @@ pub enum Id {
S3SecurityToken, S3SecurityToken,
S3SessionToken, S3SessionToken,
SmbShare, SmbShare,
#[cfg(unix)] #[cfg(posix)]
SmbWorkgroup, SmbWorkgroup,
Subtitle,
Title,
Username, Username,
WaitPopup,
WebDAVUri, WebDAVUri,
WindowSizeError,
} }
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub enum Msg { enum Msg {
Form(FormMsg), Form(FormMsg),
Ui(UiMsg), Ui(UiMsg),
None, None,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum FormMsg { enum FormMsg {
Connect, Connect,
DeleteBookmark, DeleteBookmark,
DeleteRecent, DeleteRecent,
@@ -98,15 +115,14 @@ pub enum FormMsg {
InstallUpdate, InstallUpdate,
LoadBookmark(usize), LoadBookmark(usize),
LoadRecent(usize), LoadRecent(usize),
ProtocolChanged(FileTransferProtocol), HostBridgeProtocolChanged(HostBridgeProtocol),
RemoteProtocolChanged(FileTransferProtocol),
Quit, Quit,
SaveBookmark, SaveBookmark(FormTab),
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum UiMsg { pub enum UiMsg {
AddressBlurDown,
AddressBlurUp,
BookmarksListBlur, BookmarksListBlur,
BookmarksTabBlur, BookmarksTabBlur,
CloseDeleteBookmark, CloseDeleteBookmark,
@@ -117,6 +133,25 @@ pub enum UiMsg {
CloseKeybindingsPopup, CloseKeybindingsPopup,
CloseQuitPopup, CloseQuitPopup,
CloseSaveBookmark, CloseSaveBookmark,
HostBridge(UiAuthFormMsg),
RececentsListBlur,
Remote(UiAuthFormMsg),
BookmarkNameBlur,
SaveBookmarkPasswordBlur,
ShowDeleteBookmarkPopup,
ShowDeleteRecentPopup,
ShowKeybindingsPopup,
ShowQuitPopup,
ShowReleaseNotes,
ShowSaveBookmarkPopup,
WindowResized,
}
#[derive(Debug, PartialEq, Eq)]
pub enum UiAuthFormMsg {
AddressBlurDown,
AddressBlurUp,
ChangeFormTab,
KubeNamespaceBlurDown, KubeNamespaceBlurDown,
KubeNamespaceBlurUp, KubeNamespaceBlurUp,
KubeClusterUrlBlurDown, KubeClusterUrlBlurDown,
@@ -136,7 +171,6 @@ pub enum UiMsg {
PortBlurUp, PortBlurUp,
ProtocolBlurDown, ProtocolBlurDown,
ProtocolBlurUp, ProtocolBlurUp,
RececentsListBlur,
RemoteDirectoryBlurDown, RemoteDirectoryBlurDown,
RemoteDirectoryBlurUp, RemoteDirectoryBlurUp,
S3AccessKeyBlurDown, S3AccessKeyBlurDown,
@@ -159,23 +193,14 @@ pub enum UiMsg {
S3SessionTokenBlurUp, S3SessionTokenBlurUp,
SmbShareBlurDown, SmbShareBlurDown,
SmbShareBlurUp, SmbShareBlurUp,
#[cfg(unix)] #[cfg(posix)]
SmbWorkgroupDown, SmbWorkgroupDown,
#[cfg(unix)] #[cfg(posix)]
SmbWorkgroupUp, SmbWorkgroupUp,
BookmarkNameBlur,
SaveBookmarkPasswordBlur,
ShowDeleteBookmarkPopup,
ShowDeleteRecentPopup,
ShowKeybindingsPopup,
ShowQuitPopup,
ShowReleaseNotes,
ShowSaveBookmarkPopup,
UsernameBlurDown, UsernameBlurDown,
UsernameBlurUp, UsernameBlurUp,
WebDAVUriBlurDown, WebDAVUriBlurDown,
WebDAVUriBlurUp, WebDAVUriBlurUp,
WindowResized,
} }
/// Auth form input mask /// Auth form input mask
@@ -184,10 +209,23 @@ enum InputMask {
Generic, Generic,
AwsS3, AwsS3,
Kube, Kube,
Localhost,
Smb, Smb,
WebDAV, WebDAV,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum HostBridgeProtocol {
Localhost,
Remote(FileTransferProtocol),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FormTab {
HostBridge,
Remote,
}
// Store keys // Store keys
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION"; const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES"; const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
@@ -203,8 +241,11 @@ pub struct AuthActivity {
exit_reason: Option<ExitReason>, exit_reason: Option<ExitReason>,
/// Should redraw ui /// Should redraw ui
redraw: bool, redraw: bool,
/// Protocol /// Host bridge protocol
protocol: FileTransferProtocol, host_bridge_protocol: HostBridgeProtocol,
last_form_tab: FormTab,
/// Remote file transfer protocol
remote_protocol: FileTransferProtocol,
context: Option<Context>, context: Option<Context>,
} }
@@ -214,15 +255,17 @@ impl AuthActivity {
AuthActivity { AuthActivity {
app: Application::init( app: Application::init(
EventListenerCfg::default() EventListenerCfg::default()
.default_input_listener(ticks) .crossterm_input_listener(ticks, CROSSTERM_MAX_POLL)
.poll_timeout(ticks), .poll_timeout(ticks),
), ),
context: None, context: None,
bookmarks_list: Vec::new(), bookmarks_list: Vec::new(),
exit_reason: None, exit_reason: None,
last_form_tab: FormTab::Remote,
recents_list: Vec::new(), recents_list: Vec::new(),
redraw: true, redraw: true,
protocol: FileTransferProtocol::Sftp, host_bridge_protocol: HostBridgeProtocol::Localhost,
remote_protocol: FileTransferProtocol::Sftp,
} }
} }
@@ -255,8 +298,23 @@ impl AuthActivity {
} }
/// Get current input mask to show /// Get current input mask to show
fn input_mask(&self) -> InputMask { fn remote_input_mask(&self) -> InputMask {
match self.protocol { Self::file_transfer_protocol_input_mask(self.remote_protocol)
}
/// Get current input mask to show
fn host_bridge_input_mask(&self) -> InputMask {
match self.host_bridge_protocol {
HostBridgeProtocol::Localhost => InputMask::Localhost,
HostBridgeProtocol::Remote(protocol) => {
Self::file_transfer_protocol_input_mask(protocol)
}
}
}
/// Get input mask for protocol
fn file_transfer_protocol_input_mask(protocol: FileTransferProtocol) -> InputMask {
match protocol {
FileTransferProtocol::AwsS3 => InputMask::AwsS3, FileTransferProtocol::AwsS3 => InputMask::AwsS3,
FileTransferProtocol::Ftp(_) FileTransferProtocol::Ftp(_)
| FileTransferProtocol::Scp | FileTransferProtocol::Scp
@@ -275,7 +333,7 @@ impl Activity for AuthActivity {
fn on_create(&mut self, mut context: Context) { fn on_create(&mut self, mut context: Context) {
debug!("Initializing activity"); debug!("Initializing activity");
// Initialize file transfer params // Initialize file transfer params
context.set_ftparams(FileTransferParams::default()); context.set_remote_params(FileTransferParams::default());
// Set context // Set context
self.context = Some(context); self.context = Some(context);
// Clear terminal // Clear terminal

View File

@@ -4,7 +4,10 @@
use tuirealm::{State, StateValue}; use tuirealm::{State, StateValue};
use super::{AuthActivity, ExitReason, FormMsg, Id, InputMask, Msg, UiMsg, Update}; use super::{
AuthActivity, AuthFormId, ExitReason, FormMsg, FormTab, HostBridgeProtocol, Id, InputMask, Msg,
UiAuthFormMsg, UiMsg, Update,
};
impl Update<Msg> for AuthActivity { impl Update<Msg> for AuthActivity {
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> { fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
@@ -21,19 +24,26 @@ impl AuthActivity {
fn update_form(&mut self, msg: FormMsg) -> Option<Msg> { fn update_form(&mut self, msg: FormMsg) -> Option<Msg> {
match msg { match msg {
FormMsg::Connect => { FormMsg::Connect => {
match self.collect_host_params() { let Ok(remote_params) = self.collect_remote_host_params() else {
Err(err) => { // mount error
// mount error self.mount_error("Invalid remote params parameters");
self.mount_error(err); return None;
} };
Ok(params) => {
self.save_recent(); let Ok(host_bridge_params) = self.collect_host_bridge_params() else {
// Set file transfer params to context // mount error
self.context_mut().set_ftparams(params); self.mount_error("Invalid host bridge params parameters");
// Set exit reason return None;
self.exit_reason = Some(super::ExitReason::Connect); };
}
} self.save_recent();
// Set file transfer params to context
self.context_mut().set_remote_params(remote_params);
// set host bridge params
self.context_mut()
.set_host_bridge_params(host_bridge_params);
// Set exit reason
self.exit_reason = Some(super::ExitReason::Connect);
} }
FormMsg::DeleteBookmark => { FormMsg::DeleteBookmark => {
if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::BookmarksList) { if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::BookmarksList) {
@@ -62,50 +72,86 @@ impl AuthActivity {
self.install_update(); self.install_update();
} }
FormMsg::LoadBookmark(i) => { FormMsg::LoadBookmark(i) => {
self.load_bookmark(i); self.load_bookmark(self.last_form_tab, i);
// Give focus to input password (or to protocol if not generic) // Give focus to input password (or to protocol if not generic)
assert!(self let focus = match self.last_form_tab {
.app FormTab::Remote => match self.remote_input_mask() {
.active(match self.input_mask() { InputMask::Localhost => &Id::Remote(AuthFormId::LocalDirectory),
InputMask::Generic => &Id::Password, InputMask::Generic => &Id::Remote(AuthFormId::Password),
InputMask::Smb => &Id::Password, InputMask::Smb => &Id::Remote(AuthFormId::Password),
InputMask::AwsS3 => &Id::S3Bucket, InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket),
InputMask::Kube => &Id::KubeNamespace, InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace),
InputMask::WebDAV => &Id::Password, InputMask::WebDAV => &Id::Remote(AuthFormId::Password),
}) },
.is_ok()); FormTab::HostBridge => match self.host_bridge_input_mask() {
InputMask::Localhost => &Id::HostBridge(AuthFormId::LocalDirectory),
InputMask::Generic => &Id::HostBridge(AuthFormId::Password),
InputMask::Smb => &Id::HostBridge(AuthFormId::Password),
InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3Bucket),
InputMask::Kube => &Id::HostBridge(AuthFormId::KubeNamespace),
InputMask::WebDAV => &Id::HostBridge(AuthFormId::Password),
},
};
assert!(self.app.active(focus).is_ok());
} }
FormMsg::LoadRecent(i) => { FormMsg::LoadRecent(i) => {
self.load_recent(i); self.load_recent(self.last_form_tab, i);
// Give focus to input password (or to protocol if not generic) // Give focus to input password (or to protocol if not generic)
assert!(self let focus = match self.last_form_tab {
.app FormTab::Remote => match self.remote_input_mask() {
.active(match self.input_mask() { InputMask::Localhost => &Id::Remote(AuthFormId::LocalDirectory),
InputMask::Generic => &Id::Password, InputMask::Generic => &Id::Remote(AuthFormId::Password),
InputMask::Smb => &Id::Password, InputMask::Smb => &Id::Remote(AuthFormId::Password),
InputMask::AwsS3 => &Id::S3Bucket, InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket),
InputMask::Kube => &Id::KubeNamespace, InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace),
InputMask::WebDAV => &Id::Password, InputMask::WebDAV => &Id::Remote(AuthFormId::Password),
}) },
.is_ok()); FormTab::HostBridge => match self.host_bridge_input_mask() {
InputMask::Localhost => &Id::HostBridge(AuthFormId::LocalDirectory),
InputMask::Generic => &Id::HostBridge(AuthFormId::Password),
InputMask::Smb => &Id::HostBridge(AuthFormId::Password),
InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3Bucket),
InputMask::Kube => &Id::HostBridge(AuthFormId::KubeNamespace),
InputMask::WebDAV => &Id::HostBridge(AuthFormId::Password),
},
};
assert!(self.app.active(focus).is_ok());
} }
FormMsg::ProtocolChanged(protocol) => { FormMsg::HostBridgeProtocolChanged(protocol) => {
self.protocol = protocol; self.host_bridge_protocol = protocol;
// Update port // Update port
let port: u16 = self.get_input_port(); let port: u16 = self.get_input_port(FormTab::HostBridge);
if let HostBridgeProtocol::Remote(remote_protocol) = protocol {
if Self::is_port_standard(port) {
self.mount_port(
FormTab::HostBridge,
Self::get_default_port_for_protocol(remote_protocol),
);
}
}
}
FormMsg::RemoteProtocolChanged(protocol) => {
self.remote_protocol = protocol;
// Update port
let port: u16 = self.get_input_port(FormTab::Remote);
if Self::is_port_standard(port) { if Self::is_port_standard(port) {
self.mount_port(Self::get_default_port_for_protocol(protocol)); self.mount_port(
FormTab::Remote,
Self::get_default_port_for_protocol(protocol),
);
} }
} }
FormMsg::Quit => { FormMsg::Quit => {
self.exit_reason = Some(ExitReason::Quit); self.exit_reason = Some(ExitReason::Quit);
} }
FormMsg::SaveBookmark => { FormMsg::SaveBookmark(form_tab) => {
// get bookmark name // get bookmark name
let (name, save_password) = self.get_new_bookmark(); let (name, save_password) = self.get_new_bookmark();
// Save bookmark // Save bookmark
if !name.is_empty() { if !name.is_empty() {
self.save_bookmark(name, save_password); self.save_bookmark(form_tab, name, save_password);
} }
// Umount popup // Umount popup
self.umount_bookmark_save_dialog(); self.umount_bookmark_save_dialog();
@@ -118,16 +164,30 @@ impl AuthActivity {
fn update_ui(&mut self, msg: UiMsg) -> Option<Msg> { fn update_ui(&mut self, msg: UiMsg) -> Option<Msg> {
match msg { match msg {
UiMsg::AddressBlurDown => { UiMsg::HostBridge(UiAuthFormMsg::AddressBlurDown) => {
let id = if cfg!(windows) && self.input_mask() == InputMask::Smb { let id = if cfg!(windows) && self.host_bridge_input_mask() == InputMask::Smb {
&Id::SmbShare &Id::HostBridge(AuthFormId::SmbShare)
} else { } else {
&Id::Port &Id::HostBridge(AuthFormId::Port)
}; };
assert!(self.app.active(id).is_ok()); assert!(self.app.active(id).is_ok());
} }
UiMsg::AddressBlurUp => { UiMsg::Remote(UiAuthFormMsg::AddressBlurDown) => {
assert!(self.app.active(&Id::Protocol).is_ok()); let id = if cfg!(windows) && self.remote_input_mask() == InputMask::Smb {
&Id::Remote(AuthFormId::SmbShare)
} else {
&Id::Remote(AuthFormId::Port)
};
assert!(self.app.active(id).is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::AddressBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Protocol))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::AddressBlurUp) => {
assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok());
} }
UiMsg::BookmarksListBlur => { UiMsg::BookmarksListBlur => {
assert!(self.app.active(&Id::RecentsList).is_ok()); assert!(self.app.active(&Id::RecentsList).is_ok());
@@ -136,7 +196,21 @@ impl AuthActivity {
assert!(self.app.active(&Id::BookmarkSavePassword).is_ok()); assert!(self.app.active(&Id::BookmarkSavePassword).is_ok());
} }
UiMsg::BookmarksTabBlur => { UiMsg::BookmarksTabBlur => {
assert!(self.app.active(&Id::Protocol).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Protocol))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::ChangeFormTab) => {
self.last_form_tab = FormTab::Remote;
assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok());
}
UiMsg::Remote(UiAuthFormMsg::ChangeFormTab) => {
self.last_form_tab = FormTab::HostBridge;
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Protocol))
.is_ok());
} }
UiMsg::CloseDeleteBookmark => { UiMsg::CloseDeleteBookmark => {
assert!(self.app.umount(&Id::DeleteBookmarkPopup).is_ok()); assert!(self.app.umount(&Id::DeleteBookmarkPopup).is_ok());
@@ -162,185 +236,554 @@ impl AuthActivity {
assert!(self.app.umount(&Id::BookmarkName).is_ok()); assert!(self.app.umount(&Id::BookmarkName).is_ok());
assert!(self.app.umount(&Id::BookmarkSavePassword).is_ok()); assert!(self.app.umount(&Id::BookmarkSavePassword).is_ok());
} }
UiMsg::LocalDirectoryBlurDown => { UiMsg::HostBridge(UiAuthFormMsg::LocalDirectoryBlurDown) => {
assert!(self.app.active(&Id::Protocol).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Protocol))
.is_ok());
} }
UiMsg::LocalDirectoryBlurUp => { UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurDown) => {
assert!(self.app.active(&Id::RemoteDirectory).is_ok()); assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok());
} }
UiMsg::ParamsFormBlur => { UiMsg::HostBridge(UiAuthFormMsg::LocalDirectoryBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::RemoteDirectory))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurUp) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::RemoteDirectory))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur) => {
assert!(self.app.active(&Id::BookmarksList).is_ok()); assert!(self.app.active(&Id::BookmarksList).is_ok());
} }
UiMsg::PasswordBlurDown => { UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur) => {
assert!(self.app.active(&Id::BookmarksList).is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurDown) => {
assert!(self assert!(self
.app .app
.active(match self.input_mask() { .active(match self.host_bridge_input_mask() {
InputMask::Generic => &Id::RemoteDirectory, InputMask::Localhost => unreachable!(),
#[cfg(unix)] InputMask::Generic => &Id::HostBridge(AuthFormId::RemoteDirectory),
InputMask::Smb => &Id::SmbWorkgroup, #[cfg(posix)]
#[cfg(windows)] InputMask::Smb => &Id::HostBridge(AuthFormId::SmbWorkgroup),
InputMask::Smb => &Id::RemoteDirectory, #[cfg(win)]
InputMask::AwsS3 => panic!("this shouldn't happen (password on s3)"), InputMask::Smb => &Id::HostBridge(AuthFormId::RemoteDirectory),
InputMask::Kube => panic!("this shouldn't happen (password on kube)"), InputMask::AwsS3 => unreachable!("this shouldn't happen (password on s3)"),
InputMask::WebDAV => &Id::RemoteDirectory, InputMask::Kube => unreachable!("this shouldn't happen (password on kube)"),
InputMask::WebDAV => &Id::HostBridge(AuthFormId::RemoteDirectory),
}) })
.is_ok()); .is_ok());
} }
UiMsg::PasswordBlurUp => { UiMsg::Remote(UiAuthFormMsg::PasswordBlurDown) => {
assert!(self.app.active(&Id::Username).is_ok());
}
UiMsg::PortBlurDown => {
assert!(self assert!(self
.app .app
.active(match self.input_mask() { .active(match self.remote_input_mask() {
InputMask::Generic => &Id::Username, InputMask::Localhost => unreachable!(),
InputMask::Smb => &Id::SmbShare, InputMask::Generic => &Id::Remote(AuthFormId::RemoteDirectory),
InputMask::AwsS3 | InputMask::Kube | InputMask::WebDAV => #[cfg(posix)]
panic!("this shouldn't happen (port on s3/kube/webdav)"), InputMask::Smb => &Id::Remote(AuthFormId::SmbWorkgroup),
#[cfg(win)]
InputMask::Smb => &Id::Remote(AuthFormId::RemoteDirectory),
InputMask::AwsS3 => unreachable!("this shouldn't happen (password on s3)"),
InputMask::Kube => unreachable!("this shouldn't happen (password on kube)"),
InputMask::WebDAV => &Id::Remote(AuthFormId::RemoteDirectory),
}) })
.is_ok()); .is_ok());
} }
UiMsg::PortBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurUp) => {
assert!(self.app.active(&Id::Address).is_ok());
}
UiMsg::ProtocolBlurDown => {
assert!(self assert!(self
.app .app
.active(match self.input_mask() { .active(&Id::HostBridge(AuthFormId::Username))
InputMask::Generic => &Id::Address, .is_ok());
InputMask::Smb => &Id::Address, }
InputMask::AwsS3 => &Id::S3Bucket, UiMsg::Remote(UiAuthFormMsg::PasswordBlurUp) => {
InputMask::Kube => &Id::KubeNamespace, assert!(self.app.active(&Id::Remote(AuthFormId::Username)).is_ok());
InputMask::WebDAV => &Id::WebDAVUri, }
UiMsg::HostBridge(UiAuthFormMsg::PortBlurDown) => {
assert!(self
.app
.active(match self.host_bridge_input_mask() {
InputMask::Generic => &Id::HostBridge(AuthFormId::Username),
InputMask::Smb => &Id::HostBridge(AuthFormId::SmbShare),
InputMask::Localhost
| InputMask::AwsS3
| InputMask::Kube
| InputMask::WebDAV =>
unreachable!("this shouldn't happen (port on s3/kube/webdav)"),
}) })
.is_ok()); .is_ok());
} }
UiMsg::ProtocolBlurUp => { UiMsg::Remote(UiAuthFormMsg::PortBlurDown) => {
assert!(self.app.active(&Id::LocalDirectory).is_ok()); assert!(self
.app
.active(match self.remote_input_mask() {
InputMask::Generic => &Id::Remote(AuthFormId::Username),
InputMask::Smb => &Id::Remote(AuthFormId::SmbShare),
InputMask::Localhost
| InputMask::AwsS3
| InputMask::Kube
| InputMask::WebDAV =>
unreachable!("this shouldn't happen (port on s3/kube/webdav)"),
})
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::PortBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Address))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::PortBlurUp) => {
assert!(self.app.active(&Id::Remote(AuthFormId::Address)).is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurDown) => {
assert!(self
.app
.active(match self.host_bridge_input_mask() {
InputMask::Localhost => &Id::HostBridge(AuthFormId::LocalDirectory),
InputMask::Generic => &Id::HostBridge(AuthFormId::Address),
InputMask::Smb => &Id::HostBridge(AuthFormId::Address),
InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3Bucket),
InputMask::Kube => &Id::HostBridge(AuthFormId::KubeNamespace),
InputMask::WebDAV => &Id::HostBridge(AuthFormId::WebDAVUri),
})
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::ProtocolBlurDown) => {
assert!(self
.app
.active(match self.remote_input_mask() {
InputMask::Localhost => &Id::Remote(AuthFormId::LocalDirectory),
InputMask::Generic => &Id::Remote(AuthFormId::Address),
InputMask::Smb => &Id::Remote(AuthFormId::Address),
InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket),
InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace),
InputMask::WebDAV => &Id::Remote(AuthFormId::WebDAVUri),
})
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::LocalDirectory))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::ProtocolBlurUp) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::LocalDirectory))
.is_ok());
} }
UiMsg::RececentsListBlur => { UiMsg::RececentsListBlur => {
assert!(self.app.active(&Id::BookmarksList).is_ok()); assert!(self.app.active(&Id::BookmarksList).is_ok());
} }
UiMsg::RemoteDirectoryBlurDown => { UiMsg::HostBridge(UiAuthFormMsg::RemoteDirectoryBlurDown) => {
assert!(self.app.active(&Id::LocalDirectory).is_ok());
}
UiMsg::RemoteDirectoryBlurUp => {
assert!(self assert!(self
.app .app
.active(match self.input_mask() { .active(&Id::HostBridge(AuthFormId::LocalDirectory))
InputMask::Generic => &Id::Password, .is_ok());
#[cfg(unix)] }
InputMask::Smb => &Id::SmbWorkgroup, UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurDown) => {
#[cfg(windows)] assert!(self
InputMask::Smb => &Id::Password, .app
InputMask::Kube => &Id::KubeClientKey, .active(&Id::Remote(AuthFormId::LocalDirectory))
InputMask::AwsS3 => &Id::S3NewPathStyle, .is_ok());
InputMask::WebDAV => &Id::Password, }
UiMsg::HostBridge(UiAuthFormMsg::RemoteDirectoryBlurUp) => {
assert!(self
.app
.active(match self.host_bridge_input_mask() {
InputMask::Localhost => unreachable!(),
InputMask::Generic => &Id::HostBridge(AuthFormId::Password),
#[cfg(posix)]
InputMask::Smb => &Id::HostBridge(AuthFormId::SmbWorkgroup),
#[cfg(win)]
InputMask::Smb => &Id::HostBridge(AuthFormId::Password),
InputMask::Kube => &Id::HostBridge(AuthFormId::KubeClientKey),
InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3NewPathStyle),
InputMask::WebDAV => &Id::HostBridge(AuthFormId::Password),
}) })
.is_ok()); .is_ok());
} }
UiMsg::S3BucketBlurDown => { UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurUp) => {
assert!(self.app.active(&Id::S3Region).is_ok()); assert!(self
.app
.active(match self.remote_input_mask() {
InputMask::Localhost => unreachable!(),
InputMask::Generic => &Id::Remote(AuthFormId::Password),
#[cfg(posix)]
InputMask::Smb => &Id::Remote(AuthFormId::SmbWorkgroup),
#[cfg(win)]
InputMask::Smb => &Id::Remote(AuthFormId::Password),
InputMask::Kube => &Id::Remote(AuthFormId::KubeClientKey),
InputMask::AwsS3 => &Id::Remote(AuthFormId::S3NewPathStyle),
InputMask::WebDAV => &Id::Remote(AuthFormId::Password),
})
.is_ok());
} }
UiMsg::S3BucketBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurDown) => {
assert!(self.app.active(&Id::Protocol).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3Region))
.is_ok());
} }
UiMsg::S3RegionBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3BucketBlurDown) => {
assert!(self.app.active(&Id::S3Endpoint).is_ok()); assert!(self.app.active(&Id::Remote(AuthFormId::S3Region)).is_ok());
} }
UiMsg::S3RegionBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurUp) => {
assert!(self.app.active(&Id::S3Bucket).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Protocol))
.is_ok());
} }
UiMsg::S3EndpointBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3BucketBlurUp) => {
assert!(self.app.active(&Id::S3Profile).is_ok()); assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok());
} }
UiMsg::S3EndpointBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurDown) => {
assert!(self.app.active(&Id::S3Region).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3Endpoint))
.is_ok());
} }
UiMsg::S3ProfileBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3RegionBlurDown) => {
assert!(self.app.active(&Id::S3AccessKey).is_ok()); assert!(self.app.active(&Id::Remote(AuthFormId::S3Endpoint)).is_ok());
} }
UiMsg::S3ProfileBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurUp) => {
assert!(self.app.active(&Id::S3Endpoint).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3Bucket))
.is_ok());
} }
UiMsg::S3AccessKeyBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3RegionBlurUp) => {
assert!(self.app.active(&Id::S3SecretAccessKey).is_ok()); assert!(self.app.active(&Id::Remote(AuthFormId::S3Bucket)).is_ok());
} }
UiMsg::S3AccessKeyBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurDown) => {
assert!(self.app.active(&Id::S3Profile).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3Profile))
.is_ok());
} }
UiMsg::S3SecretAccessKeyBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurDown) => {
assert!(self.app.active(&Id::S3SecurityToken).is_ok()); assert!(self.app.active(&Id::Remote(AuthFormId::S3Profile)).is_ok());
} }
UiMsg::S3SecretAccessKeyBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurUp) => {
assert!(self.app.active(&Id::S3AccessKey).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3Region))
.is_ok());
} }
UiMsg::S3SecurityTokenBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurUp) => {
assert!(self.app.active(&Id::S3SessionToken).is_ok()); assert!(self.app.active(&Id::Remote(AuthFormId::S3Region)).is_ok());
} }
UiMsg::S3SecurityTokenBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurDown) => {
assert!(self.app.active(&Id::S3SecretAccessKey).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3AccessKey))
.is_ok());
} }
UiMsg::S3SessionTokenBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurDown) => {
assert!(self.app.active(&Id::S3NewPathStyle).is_ok()); assert!(self
.app
.active(&Id::Remote(AuthFormId::S3AccessKey))
.is_ok());
} }
UiMsg::S3SessionTokenBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurUp) => {
assert!(self.app.active(&Id::S3SecurityToken).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3Endpoint))
.is_ok());
} }
UiMsg::S3NewPathStyleBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurUp) => {
assert!(self.app.active(&Id::RemoteDirectory).is_ok()); assert!(self.app.active(&Id::Remote(AuthFormId::S3Endpoint)).is_ok());
} }
UiMsg::S3NewPathStyleBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurDown) => {
assert!(self.app.active(&Id::S3SessionToken).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3SecretAccessKey))
.is_ok());
} }
UiMsg::KubeClientCertBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurDown) => {
assert!(self.app.active(&Id::KubeClientKey).is_ok()); assert!(self
.app
.active(&Id::Remote(AuthFormId::S3SecretAccessKey))
.is_ok());
} }
UiMsg::KubeClientCertBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurUp) => {
assert!(self.app.active(&Id::KubeUsername).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3Profile))
.is_ok());
} }
UiMsg::KubeClientKeyBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurUp) => {
assert!(self.app.active(&Id::RemoteDirectory).is_ok()); assert!(self.app.active(&Id::Remote(AuthFormId::S3Profile)).is_ok());
} }
UiMsg::KubeClientKeyBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3SecretAccessKeyBlurDown) => {
assert!(self.app.active(&Id::KubeClientCert).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3SecurityToken))
.is_ok());
} }
UiMsg::KubeNamespaceBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurDown) => {
assert!(self.app.active(&Id::KubeClusterUrl).is_ok()); assert!(self
.app
.active(&Id::Remote(AuthFormId::S3SecurityToken))
.is_ok());
} }
UiMsg::KubeNamespaceBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3SecretAccessKeyBlurUp) => {
assert!(self.app.active(&Id::Protocol).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3AccessKey))
.is_ok());
} }
UiMsg::KubeClusterUrlBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurUp) => {
assert!(self.app.active(&Id::KubeUsername).is_ok()); assert!(self
.app
.active(&Id::Remote(AuthFormId::S3AccessKey))
.is_ok());
} }
UiMsg::KubeClusterUrlBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3SecurityTokenBlurDown) => {
assert!(self.app.active(&Id::KubeNamespace).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3SessionToken))
.is_ok());
} }
UiMsg::KubeUsernameBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurDown) => {
assert!(self.app.active(&Id::KubeClientCert).is_ok()); assert!(self
.app
.active(&Id::Remote(AuthFormId::S3SessionToken))
.is_ok());
} }
UiMsg::KubeUsernameBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3SecurityTokenBlurUp) => {
assert!(self.app.active(&Id::KubeClusterUrl).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3SecretAccessKey))
.is_ok());
} }
UiMsg::SmbShareBlurDown => { UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurUp) => {
assert!(self.app.active(&Id::Username).is_ok()); assert!(self
.app
.active(&Id::Remote(AuthFormId::S3SecretAccessKey))
.is_ok());
} }
UiMsg::SmbShareBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::S3SessionTokenBlurDown) => {
let id = if cfg!(windows) && self.input_mask() == InputMask::Smb { assert!(self
&Id::Address .app
.active(&Id::HostBridge(AuthFormId::S3NewPathStyle))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::S3SessionTokenBlurDown) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::S3NewPathStyle))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::S3SessionTokenBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3SecurityToken))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::S3SessionTokenBlurUp) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::S3SecurityToken))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::S3NewPathStyleBlurDown) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::RemoteDirectory))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::S3NewPathStyleBlurDown) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::RemoteDirectory))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::S3NewPathStyleBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::S3SessionToken))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::S3NewPathStyleBlurUp) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::S3SessionToken))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::KubeClientCertBlurDown) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::KubeClientKey))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurDown) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::KubeClientKey))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::KubeClientCertBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::KubeUsername))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurUp) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::KubeUsername))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurDown) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::RemoteDirectory))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurDown) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::RemoteDirectory))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::KubeClientCert))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurUp) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::KubeClientCert))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurDown) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::KubeClusterUrl))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurDown) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::KubeClusterUrl))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Protocol))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurUp) => {
assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::KubeClusterUrlBlurDown) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::KubeUsername))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurDown) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::KubeUsername))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::KubeClusterUrlBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::KubeNamespace))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurUp) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::KubeNamespace))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurDown) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::KubeClientCert))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurDown) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::KubeClientCert))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::KubeClusterUrl))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurUp) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::KubeClusterUrl))
.is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::SmbShareBlurDown) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Username))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::SmbShareBlurDown) => {
assert!(self.app.active(&Id::Remote(AuthFormId::Username)).is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::SmbShareBlurUp) => {
let id = if cfg!(windows) && self.host_bridge_input_mask() == InputMask::Smb {
&Id::HostBridge(AuthFormId::Address)
} else { } else {
&Id::Port &Id::HostBridge(AuthFormId::Port)
}; };
assert!(self.app.active(id).is_ok()); assert!(self.app.active(id).is_ok());
} }
#[cfg(unix)] UiMsg::Remote(UiAuthFormMsg::SmbShareBlurUp) => {
UiMsg::SmbWorkgroupDown => { let id = if cfg!(windows) && self.remote_input_mask() == InputMask::Smb {
assert!(self.app.active(&Id::RemoteDirectory).is_ok()); &Id::Remote(AuthFormId::Address)
} else {
&Id::Remote(AuthFormId::Port)
};
assert!(self.app.active(id).is_ok());
} }
#[cfg(unix)] #[cfg(posix)]
UiMsg::SmbWorkgroupUp => { UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupDown) => {
assert!(self.app.active(&Id::Password).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::RemoteDirectory))
.is_ok());
}
#[cfg(posix)]
UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupDown) => {
assert!(self
.app
.active(&Id::Remote(AuthFormId::RemoteDirectory))
.is_ok());
}
#[cfg(posix)]
UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Password))
.is_ok());
}
#[cfg(posix)]
UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupUp) => {
assert!(self.app.active(&Id::Remote(AuthFormId::Password)).is_ok());
} }
UiMsg::SaveBookmarkPasswordBlur => { UiMsg::SaveBookmarkPasswordBlur => {
assert!(self.app.active(&Id::BookmarkName).is_ok()); assert!(self.app.active(&Id::BookmarkName).is_ok());
@@ -361,28 +804,60 @@ impl AuthActivity {
self.mount_release_notes(); self.mount_release_notes();
} }
UiMsg::ShowSaveBookmarkPopup => { UiMsg::ShowSaveBookmarkPopup => {
self.mount_bookmark_save_dialog(); self.mount_bookmark_save_dialog(self.get_current_form_tab());
} }
UiMsg::UsernameBlurDown => { UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurDown) => {
assert!(self.app.active(&Id::Password).is_ok());
}
UiMsg::UsernameBlurUp => {
assert!(self assert!(self
.app .app
.active(match self.input_mask() { .active(&Id::HostBridge(AuthFormId::Password))
InputMask::Generic => &Id::Port, .is_ok());
InputMask::Smb => &Id::SmbShare, }
InputMask::Kube => panic!("this shouldn't happen (username on kube)"), UiMsg::Remote(UiAuthFormMsg::UsernameBlurDown) => {
InputMask::AwsS3 => panic!("this shouldn't happen (username on s3)"), assert!(self.app.active(&Id::Remote(AuthFormId::Password)).is_ok());
InputMask::WebDAV => &Id::WebDAVUri, }
UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurUp) => {
assert!(self
.app
.active(match self.host_bridge_input_mask() {
InputMask::Localhost => unreachable!(),
InputMask::Generic => &Id::HostBridge(AuthFormId::Port),
InputMask::Smb => &Id::HostBridge(AuthFormId::SmbShare),
InputMask::Kube => unreachable!("this shouldn't happen (username on kube)"),
InputMask::AwsS3 => unreachable!("this shouldn't happen (username on s3)"),
InputMask::WebDAV => &Id::HostBridge(AuthFormId::WebDAVUri),
}) })
.is_ok()); .is_ok());
} }
UiMsg::WebDAVUriBlurDown => { UiMsg::Remote(UiAuthFormMsg::UsernameBlurUp) => {
assert!(self.app.active(&Id::Username).is_ok()); assert!(self
.app
.active(match self.remote_input_mask() {
InputMask::Localhost => unreachable!(),
InputMask::Generic => &Id::Remote(AuthFormId::Port),
InputMask::Smb => &Id::Remote(AuthFormId::SmbShare),
InputMask::Kube => unreachable!("this shouldn't happen (username on kube)"),
InputMask::AwsS3 => unreachable!("this shouldn't happen (username on s3)"),
InputMask::WebDAV => &Id::Remote(AuthFormId::WebDAVUri),
})
.is_ok());
} }
UiMsg::WebDAVUriBlurUp => { UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurDown) => {
assert!(self.app.active(&Id::Protocol).is_ok()); assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Username))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::WebDAVUriBlurDown) => {
assert!(self.app.active(&Id::Remote(AuthFormId::Username)).is_ok());
}
UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurUp) => {
assert!(self
.app
.active(&Id::HostBridge(AuthFormId::Protocol))
.is_ok());
}
UiMsg::Remote(UiAuthFormMsg::WebDAVUriBlurUp) => {
assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok());
} }
UiMsg::WindowResized => { UiMsg::WindowResized => {
self.redraw = true; self.redraw = true;

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ enum SyncBrowsingDestination {
impl FileTransferActivity { impl FileTransferActivity {
/// Enter a directory on local host from entry /// Enter a directory on local host from entry
pub(crate) fn action_enter_local_dir(&mut self, dir: File) { pub(crate) fn action_enter_local_dir(&mut self, dir: File) {
self.local_changedir(dir.path(), true); self.host_bridge_changedir(dir.path(), true);
if self.browser.sync_browsing && self.browser.found().is_none() { if self.browser.sync_browsing && self.browser.found().is_none() {
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name())); self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
} }
@@ -35,8 +35,9 @@ impl FileTransferActivity {
/// Change local directory reading value from input /// Change local directory reading value from input
pub(crate) fn action_change_local_dir(&mut self, input: String) { pub(crate) fn action_change_local_dir(&mut self, input: String) {
let dir_path: PathBuf = self.local_to_abs_path(PathBuf::from(input.as_str()).as_path()); let dir_path: PathBuf =
self.local_changedir(dir_path.as_path(), true); self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
self.host_bridge_changedir(dir_path.as_path(), true);
// Check whether to sync // Check whether to sync
if self.browser.sync_browsing && self.browser.found().is_none() { if self.browser.sync_browsing && self.browser.found().is_none() {
self.synchronize_browsing(SyncBrowsingDestination::Path(input)); self.synchronize_browsing(SyncBrowsingDestination::Path(input));
@@ -55,8 +56,8 @@ impl FileTransferActivity {
/// Go to previous directory from localhost /// Go to previous directory from localhost
pub(crate) fn action_go_to_previous_local_dir(&mut self) { pub(crate) fn action_go_to_previous_local_dir(&mut self) {
if let Some(d) = self.local_mut().popd() { if let Some(d) = self.host_bridge_mut().popd() {
self.local_changedir(d.as_path(), false); self.host_bridge_changedir(d.as_path(), false);
// Check whether to sync // Check whether to sync
if self.browser.sync_browsing && self.browser.found().is_none() { if self.browser.sync_browsing && self.browser.found().is_none() {
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir); self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
@@ -78,10 +79,10 @@ impl FileTransferActivity {
/// Go to upper directory on local host /// Go to upper directory on local host
pub(crate) fn action_go_to_local_upper_dir(&mut self) { pub(crate) fn action_go_to_local_upper_dir(&mut self) {
// Get pwd // Get pwd
let path: PathBuf = self.local().wrkdir.clone(); let path: PathBuf = self.host_bridge().wrkdir.clone();
// Go to parent directory // Go to parent directory
if let Some(parent) = path.as_path().parent() { if let Some(parent) = path.as_path().parent() {
self.local_changedir(parent, true); self.host_bridge_changedir(parent, true);
// If sync is enabled update remote too // If sync is enabled update remote too
if self.browser.sync_browsing && self.browser.found().is_none() { if self.browser.sync_browsing && self.browser.found().is_none() {
self.synchronize_browsing(SyncBrowsingDestination::ParentDir); self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
@@ -118,7 +119,7 @@ impl FileTransferActivity {
trace!("Synchronizing browsing to path {}", path.display()); trace!("Synchronizing browsing to path {}", path.display());
// Check whether destination exists on host // Check whether destination exists on host
let exists = match self.browser.tab() { let exists = match self.browser.tab() {
FileExplorerTab::Local => match self.client.exists(path.as_path()) { FileExplorerTab::HostBridge => match self.client.exists(path.as_path()) {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(err) => {
error!( error!(
@@ -129,7 +130,17 @@ impl FileTransferActivity {
return; return;
} }
}, },
FileExplorerTab::Remote => self.host.file_exists(path.as_path()), FileExplorerTab::Remote => match self.host_bridge.exists(path.as_path()) {
Ok(e) => e,
Err(err) => {
error!(
"Failed to check whether {} exists on host: {}",
path.display(),
err
);
return;
}
},
_ => return, _ => return,
}; };
let name = path let name = path
@@ -150,7 +161,7 @@ impl FileTransferActivity {
trace!("User wants to create the unexisting directory"); trace!("User wants to create the unexisting directory");
// Make directory // Make directory
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_remote_mkdir(name.clone()), FileExplorerTab::HostBridge => self.action_remote_mkdir(name.clone()),
FileExplorerTab::Remote => self.action_local_mkdir(name.clone()), FileExplorerTab::Remote => self.action_local_mkdir(name.clone()),
_ => {} _ => {}
} }
@@ -173,18 +184,18 @@ impl FileTransferActivity {
// Enter directory // Enter directory
match destination { match destination {
SyncBrowsingDestination::ParentDir => match self.browser.tab() { SyncBrowsingDestination::ParentDir => match self.browser.tab() {
FileExplorerTab::Local => self.remote_changedir(path.as_path(), true), FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
FileExplorerTab::Remote => self.local_changedir(path.as_path(), true), FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
_ => {} _ => {}
}, },
SyncBrowsingDestination::Path(_) => match self.browser.tab() { SyncBrowsingDestination::Path(_) => match self.browser.tab() {
FileExplorerTab::Local => self.remote_changedir(path.as_path(), true), FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
FileExplorerTab::Remote => self.local_changedir(path.as_path(), true), FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
_ => {} _ => {}
}, },
SyncBrowsingDestination::PreviousDir => match self.browser.tab() { SyncBrowsingDestination::PreviousDir => match self.browser.tab() {
FileExplorerTab::Local => self.remote_changedir(path.as_path(), false), FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), false),
FileExplorerTab::Remote => self.local_changedir(path.as_path(), false), FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), false),
_ => {} _ => {}
}, },
} }
@@ -197,13 +208,13 @@ impl FileTransferActivity {
) -> Option<PathBuf> { ) -> Option<PathBuf> {
match (destination, self.browser.tab()) { match (destination, self.browser.tab()) {
// NOTE: tab and methods are switched on purpose // NOTE: tab and methods are switched on purpose
(SyncBrowsingDestination::ParentDir, FileExplorerTab::Local) => { (SyncBrowsingDestination::ParentDir, FileExplorerTab::HostBridge) => {
self.remote().wrkdir.parent().map(|x| x.to_path_buf()) self.remote().wrkdir.parent().map(|x| x.to_path_buf())
} }
(SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => { (SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => {
self.local().wrkdir.parent().map(|x| x.to_path_buf()) self.host_bridge().wrkdir.parent().map(|x| x.to_path_buf())
} }
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Local) => { (SyncBrowsingDestination::PreviousDir, FileExplorerTab::HostBridge) => {
if let Some(p) = self.remote_mut().popd() { if let Some(p) = self.remote_mut().popd() {
Some(p) Some(p)
} else { } else {
@@ -212,7 +223,7 @@ impl FileTransferActivity {
} }
} }
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => { (SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => {
if let Some(p) = self.local_mut().popd() { if let Some(p) = self.host_bridge_mut().popd() {
Some(p) Some(p)
} else { } else {
warn!("Cannot synchronize browsing: local has no previous directory in stack"); warn!("Cannot synchronize browsing: local has no previous directory in stack");

View File

@@ -3,12 +3,11 @@ use remotefs::fs::UnixPex;
use super::{FileTransferActivity, LogLevel}; use super::{FileTransferActivity, LogLevel};
impl FileTransferActivity { impl FileTransferActivity {
#[cfg(unix)]
pub fn action_local_chmod(&mut self, mode: UnixPex) { pub fn action_local_chmod(&mut self, mode: UnixPex) {
let files = self.get_local_selected_entries().get_files(); let files = self.get_local_selected_entries().get_files();
for file in files { for file in files {
if let Err(err) = self.host.chmod(file.path(), mode) { if let Err(err) = self.host_bridge.chmod(file.path(), mode) {
self.log_and_alert( self.log_and_alert(
LogLevel::Error, LogLevel::Error,
format!( format!(
@@ -51,12 +50,11 @@ impl FileTransferActivity {
} }
} }
#[cfg(unix)]
pub fn action_find_local_chmod(&mut self, mode: UnixPex) { pub fn action_find_local_chmod(&mut self, mode: UnixPex) {
let files = self.get_found_selected_entries().get_files(); let files = self.get_found_selected_entries().get_files();
for file in files { for file in files {
if let Err(err) = self.host.chmod(file.path(), mode) { if let Err(err) = self.host_bridge.chmod(file.path(), mode) {
self.log_and_alert( self.log_and_alert(
LogLevel::Error, LogLevel::Error,
format!( format!(

View File

@@ -53,7 +53,7 @@ impl FileTransferActivity {
} }
fn local_copy_file(&mut self, entry: &File, dest: &Path) { fn local_copy_file(&mut self, entry: &File, dest: &Path) {
match self.host.copy(entry, dest) { match self.host_bridge.copy(entry, dest) {
Ok(_) => { Ok(_) => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
@@ -136,7 +136,7 @@ impl FileTransferActivity {
return Err(err); return Err(err);
} }
// Stat dir // Stat dir
let tempdir_entry = match self.host.stat(tempdir_path.as_path()) { let tempdir_entry = match self.host_bridge.stat(tempdir_path.as_path()) {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(err) => {
self.log_and_alert( self.log_and_alert(
@@ -189,7 +189,7 @@ impl FileTransferActivity {
return Err(err); return Err(err);
} }
// Get local fs entry // Get local fs entry
let tmpfile_entry = match self.host.stat(tmpfile.path()) { let tmpfile_entry = match self.host_bridge.stat(tmpfile.path()) {
Ok(e) if e.is_file() => e, Ok(e) if e.is_file() => e,
Ok(_) => panic!("{} is not a file", tmpfile.path().display()), Ok(_) => panic!("{} is not a file", tmpfile.path().display()),
Err(err) => { Err(err) => {

View File

@@ -43,7 +43,7 @@ impl FileTransferActivity {
} }
pub(crate) fn local_remove_file(&mut self, entry: &File) { pub(crate) fn local_remove_file(&mut self, entry: &File) {
match self.host.remove(entry) { match self.host_bridge.remove(entry) {
Ok(_) => { Ok(_) => {
// Log // Log
self.log( self.log(

View File

@@ -2,13 +2,12 @@
//! //!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
// locals
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::SystemTime; use std::time::SystemTime;
// ext use remotefs::fs::Metadata;
use remotefs::File; use remotefs::File;
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload}; use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
@@ -29,7 +28,12 @@ impl FileTransferActivity {
format!("Opening file \"{}\"", entry.path().display()), format!("Opening file \"{}\"", entry.path().display()),
); );
// Edit file // Edit file
if let Err(err) = self.edit_local_file(entry.path()) { let res = match self.host_bridge.is_localhost() {
true => self.edit_local_file(entry.path()).map(|_| ()),
false => self.edit_bridged_local_file(entry),
};
if let Err(err) = res {
self.log_and_alert(LogLevel::Error, err); self.log_and_alert(LogLevel::Error, err);
} }
} }
@@ -59,7 +63,83 @@ impl FileTransferActivity {
} }
/// Edit a file on localhost /// Edit a file on localhost
fn edit_local_file(&mut self, path: &Path) -> Result<(), String> { fn edit_bridged_local_file(&mut self, entry: &File) -> Result<(), String> {
// Download file
let tmpfile: String =
match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) {
None => {
return Err("Could not create tempdir".to_string());
}
Some(p) => p,
};
let cache: PathBuf = match self.cache.as_ref() {
None => {
return Err("Could not create tempdir".to_string());
}
Some(p) => p.path().to_path_buf(),
};
// open from host bridge
let mut reader = match self.host_bridge.open_file(entry.path()) {
Ok(reader) => reader,
Err(err) => {
return Err(format!("Failed to open bridged entry: {err}"));
}
};
let tempfile = cache.join(tmpfile);
// write to file
let mut writer = match std::fs::File::create(tempfile.as_path()) {
Ok(writer) => writer,
Err(err) => {
return Err(format!("Failed to write file: {err}"));
}
};
let new_file_size = match std::io::copy(&mut reader, &mut writer) {
Err(err) => return Err(format!("Could not write file: {err}")),
Ok(size) => size,
};
// edit file
let has_changed = self.edit_local_file(tempfile.as_path())?;
if has_changed {
// report changes to remote
let mut reader = match std::fs::File::open(tempfile.as_path()) {
Ok(reader) => reader,
Err(err) => {
return Err(format!("Could not open file: {err}"));
}
};
let mut writer = match self.host_bridge.create_file(
entry.path(),
&Metadata {
size: new_file_size,
..Default::default()
},
) {
Ok(writer) => writer,
Err(err) => {
return Err(format!("Could not write file: {err}"));
}
};
if let Err(err) = std::io::copy(&mut reader, &mut writer) {
return Err(format!("Could not write file: {err}"));
}
self.host_bridge
.finalize_write(writer)
.map_err(|err| format!("Could not write file: {err}"))?;
}
Ok(())
}
fn edit_local_file(&mut self, path: &Path) -> Result<bool, String> {
// Read first 2048 bytes or less from file to check if it is textual // Read first 2048 bytes or less from file to check if it is textual
match OpenOptions::new().read(true).open(path) { match OpenOptions::new().read(true).open(path) {
Ok(mut f) => { Ok(mut f) => {
@@ -90,6 +170,8 @@ impl FileTransferActivity {
} }
// Lock ports // Lock ports
assert!(self.app.lock_ports().is_ok()); assert!(self.app.lock_ports().is_ok());
// Get current file modification time
let prev_mtime = self.get_localhost_mtime(path)?;
// Open editor // Open editor
match edit::edit_file(path) { match edit::edit_file(path) {
Ok(_) => self.log( Ok(_) => self.log(
@@ -117,7 +199,23 @@ impl FileTransferActivity {
// Unlock ports // Unlock ports
assert!(self.app.unlock_ports().is_ok()); assert!(self.app.unlock_ports().is_ok());
} }
Ok(()) let after_mtime = self.get_localhost_mtime(path)?;
// return if file has changed
Ok(prev_mtime != after_mtime)
}
fn get_localhost_mtime(&self, p: &Path) -> Result<SystemTime, String> {
let attr = match std::fs::metadata(p) {
Ok(metadata) => metadata,
Err(err) => {
return Err(format!("Could not read file metadata: {}", err));
}
};
Ok(Metadata::from(attr)
.modified
.unwrap_or(std::time::UNIX_EPOCH))
} }
/// Edit file on remote host /// Edit file on remote host
@@ -138,7 +236,7 @@ impl FileTransferActivity {
return Err(format!("Could not open file {file_name}: {err}")); return Err(format!("Could not open file {file_name}: {err}"));
} }
// Get current file modification time // Get current file modification time
let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) { let prev_mtime: SystemTime = match self.host_bridge.stat(tmpfile.as_path()) {
Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH), Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH),
Err(err) => { Err(err) => {
return Err(format!( return Err(format!(
@@ -151,7 +249,7 @@ impl FileTransferActivity {
// Edit file // Edit file
self.edit_local_file(tmpfile.as_path())?; self.edit_local_file(tmpfile.as_path())?;
// Get local fs entry // Get local fs entry
let tmpfile_entry: File = match self.host.stat(tmpfile.as_path()) { let tmpfile_entry: File = match self.host_bridge.stat(tmpfile.as_path()) {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(err) => {
return Err(format!( return Err(format!(
@@ -177,7 +275,7 @@ impl FileTransferActivity {
), ),
); );
// Get local fs entry // Get local fs entry
let tmpfile_entry = match self.host.stat(tmpfile.as_path()) { let tmpfile_entry = match self.host_bridge.stat(tmpfile.as_path()) {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(err) => {
return Err(format!( return Err(format!(

View File

@@ -7,7 +7,7 @@ use super::{FileTransferActivity, LogLevel};
impl FileTransferActivity { impl FileTransferActivity {
pub(crate) fn action_local_exec(&mut self, input: String) { pub(crate) fn action_local_exec(&mut self, input: String) {
match self.host.exec(input.as_str()) { match self.host_bridge.exec(input.as_str()) {
Ok(output) => { Ok(output) => {
// Reload files // Reload files
self.log(LogLevel::Info, format!("\"{input}\": {output}")); self.log(LogLevel::Info, format!("\"{input}\": {output}"));

View File

@@ -40,7 +40,7 @@ impl FileTransferActivity {
let filter = Filter::from_str(filter).unwrap(); let filter = Filter::from_str(filter).unwrap();
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.browser.local().iter_files(), FileExplorerTab::HostBridge => self.browser.host_bridge().iter_files(),
FileExplorerTab::Remote => self.browser.remote().iter_files(), FileExplorerTab::Remote => self.browser.remote().iter_files(),
_ => return vec![], _ => return vec![],
} }

View File

@@ -24,8 +24,8 @@ impl FileTransferActivity {
}; };
// Change directory // Change directory
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => { FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
self.local_changedir(path.as_path(), true) self.host_bridge_changedir(path.as_path(), true)
} }
FileExplorerTab::FindRemote | FileExplorerTab::Remote => { FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
self.remote_changedir(path.as_path(), true) self.remote_changedir(path.as_path(), true)
@@ -36,12 +36,16 @@ impl FileTransferActivity {
pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) { pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
let wrkdir: PathBuf = match self.browser.tab() { let wrkdir: PathBuf = match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(), FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(), self.remote().wrkdir.clone()
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
self.host_bridge().wrkdir.clone()
}
}; };
match self.get_found_selected_entries() { match self.get_found_selected_entries() {
SelectedFile::One(entry) => match self.browser.tab() { SelectedFile::One(entry) => match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => { FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if self.config().get_prompt_on_file_replace() if self.config().get_prompt_on_file_replace()
&& self.remote_file_exists(file_to_check.as_path()) && self.remote_file_exists(file_to_check.as_path())
@@ -66,7 +70,7 @@ impl FileTransferActivity {
FileExplorerTab::FindRemote | FileExplorerTab::Remote => { FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if self.config().get_prompt_on_file_replace() if self.config().get_prompt_on_file_replace()
&& self.local_file_exists(file_to_check.as_path()) && self.host_bridge_file_exists(file_to_check.as_path())
&& !self.should_replace_file( && !self.should_replace_file(
opts.save_as.clone().unwrap_or_else(|| entry.name()), opts.save_as.clone().unwrap_or_else(|| entry.name()),
) )
@@ -94,7 +98,7 @@ impl FileTransferActivity {
} }
// Iter files // Iter files
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => { FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
if self.config().get_prompt_on_file_replace() { if self.config().get_prompt_on_file_replace() {
// Check which file would be replaced // Check which file would be replaced
let existing_files: Vec<&File> = entries let existing_files: Vec<&File> = entries
@@ -131,7 +135,7 @@ impl FileTransferActivity {
let existing_files: Vec<&File> = entries let existing_files: Vec<&File> = entries
.iter() .iter()
.filter(|x| { .filter(|x| {
self.local_file_exists( self.host_bridge_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(), Self::file_to_check_many(x, dest_path.as_path()).as_path(),
) )
}) })
@@ -179,7 +183,7 @@ impl FileTransferActivity {
fn remove_found_file(&mut self, entry: &File) { fn remove_found_file(&mut self, entry: &File) {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => { FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
self.local_remove_file(entry); self.local_remove_file(entry);
} }
FileExplorerTab::FindRemote | FileExplorerTab::Remote => { FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
@@ -224,7 +228,7 @@ impl FileTransferActivity {
fn open_found_file(&mut self, entry: &File, with: Option<&str>) { fn open_found_file(&mut self, entry: &File, with: Option<&str>) {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => { FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
self.action_open_local_file(entry, with); self.action_open_local_file(entry, with);
} }
FileExplorerTab::FindRemote | FileExplorerTab::Remote => { FileExplorerTab::FindRemote | FileExplorerTab::Remote => {

View File

@@ -11,7 +11,10 @@ use super::{FileTransferActivity, LogLevel};
impl FileTransferActivity { impl FileTransferActivity {
pub(crate) fn action_local_mkdir(&mut self, input: String) { pub(crate) fn action_local_mkdir(&mut self, input: String) {
match self.host.mkdir(PathBuf::from(input.as_str()).as_path()) { match self
.host_bridge
.mkdir(PathBuf::from(input.as_str()).as_path())
{
Ok(_) => { Ok(_) => {
// Reload files // Reload files
self.log(LogLevel::Info, format!("Created directory \"{input}\"")); self.log(LogLevel::Info, format!("Created directory \"{input}\""));

View File

@@ -86,12 +86,12 @@ impl From<Vec<&File>> for SelectedFile {
impl FileTransferActivity { impl FileTransferActivity {
/// Get local file entry /// Get local file entry
pub(crate) fn get_local_selected_entries(&self) -> SelectedFile { pub(crate) fn get_local_selected_entries(&self) -> SelectedFile {
match self.get_selected_index(&Id::ExplorerLocal) { match self.get_selected_index(&Id::ExplorerHostBridge) {
SelectedFileIndex::One(idx) => SelectedFile::from(self.local().get(idx)), SelectedFileIndex::One(idx) => SelectedFile::from(self.host_bridge().get(idx)),
SelectedFileIndex::Many(files) => { SelectedFileIndex::Many(files) => {
let files: Vec<&File> = files let files: Vec<&File> = files
.iter() .iter()
.filter_map(|x| self.local().get(*x)) // Usize to Option<File> .filter_map(|x| self.host_bridge().get(*x)) // Usize to Option<File>
.collect(); .collect();
SelectedFile::from(files) SelectedFile::from(files)
} }

View File

@@ -6,13 +6,15 @@
use std::fs::File as StdFile; use std::fs::File as StdFile;
use std::path::PathBuf; use std::path::PathBuf;
use remotefs::fs::Metadata;
use super::{File, FileTransferActivity, LogLevel}; use super::{File, FileTransferActivity, LogLevel};
impl FileTransferActivity { impl FileTransferActivity {
pub(crate) fn action_local_newfile(&mut self, input: String) { pub(crate) fn action_local_newfile(&mut self, input: String) {
// Check if file exists // Check if file exists
let mut file_exists: bool = false; let mut file_exists: bool = false;
for file in self.local().iter_files_all() { for file in self.host_bridge().iter_files_all() {
if input == file.name() { if input == file.name() {
file_exists = true; file_exists = true;
} }
@@ -21,19 +23,35 @@ impl FileTransferActivity {
self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",)); self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",));
return; return;
} }
// Create file // Create file
let file_path: PathBuf = PathBuf::from(input.as_str()); let file_path: PathBuf = PathBuf::from(input.as_str());
if let Err(err) = self.host.open_file_write(file_path.as_path()) { let writer = match self
.host_bridge
.create_file(file_path.as_path(), &Metadata::default())
{
Ok(f) => f,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not create file \"{}\": {}", file_path.display(), err),
);
return;
}
};
// finalize write
if let Err(err) = self.host_bridge.finalize_write(writer) {
self.log_and_alert( self.log_and_alert(
LogLevel::Error, LogLevel::Error,
format!("Could not create file \"{}\": {}", file_path.display(), err), format!("Could not write file \"{}\": {}", file_path.display(), err),
);
} else {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
); );
return;
} }
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
} }
pub(crate) fn action_remote_newfile(&mut self, input: String) { pub(crate) fn action_remote_newfile(&mut self, input: String) {
@@ -57,7 +75,7 @@ impl FileTransferActivity {
} }
Ok(tfile) => { Ok(tfile) => {
// Stat tempfile // Stat tempfile
let local_file: File = match self.host.stat(tfile.path()) { let local_file: File = match self.host_bridge.stat(tfile.path()) {
Err(err) => { Err(err) => {
self.log_and_alert( self.log_and_alert(
LogLevel::Error, LogLevel::Error,

View File

@@ -35,7 +35,11 @@ impl FileTransferActivity {
/// Perform open lopcal file /// Perform open lopcal file
pub(crate) fn action_open_local_file(&mut self, entry: &File, open_with: Option<&str>) { pub(crate) fn action_open_local_file(&mut self, entry: &File, open_with: Option<&str>) {
self.open_path_with(entry.path(), open_with); if self.host_bridge.is_localhost() {
self.open_path_with(entry.path(), open_with);
} else {
self.open_bridged_file(entry, open_with);
}
} }
/// Open remote file. The file is first downloaded to a temporary directory on localhost /// Open remote file. The file is first downloaded to a temporary directory on localhost
@@ -104,6 +108,57 @@ impl FileTransferActivity {
.for_each(|x| self.action_open_remote_file(x, Some(with))); .for_each(|x| self.action_open_remote_file(x, Some(with)));
} }
fn open_bridged_file(&mut self, entry: &File, open_with: Option<&str>) {
// Download file
let tmpfile: String =
match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) {
None => {
self.log(LogLevel::Error, String::from("Could not create tempdir"));
return;
}
Some(p) => p,
};
let cache: PathBuf = match self.cache.as_ref() {
None => {
self.log(LogLevel::Error, String::from("Could not create tempdir"));
return;
}
Some(p) => p.path().to_path_buf(),
};
let tmpfile = cache.join(tmpfile);
// open from host bridge
let mut reader = match self.host_bridge.open_file(entry.path()) {
Ok(reader) => reader,
Err(err) => {
self.log(
LogLevel::Error,
format!("Failed to open bridged entry: {err}"),
);
return;
}
};
// write to file
let mut writer = match std::fs::File::create(tmpfile.as_path()) {
Ok(writer) => writer,
Err(err) => {
self.log(LogLevel::Error, format!("Failed to create file: {err}"));
return;
}
};
if let Err(err) = std::io::copy(&mut reader, &mut writer) {
self.log(LogLevel::Error, format!("Failed to write file: {err}"));
return;
}
if tmpfile.exists() {
self.open_path_with(tmpfile.as_path(), open_with);
}
}
/// Common function which opens a path with default or specified program. /// Common function which opens a path with default or specified program.
fn open_path_with(&mut self, p: &Path, with: Option<&str>) { fn open_path_with(&mut self, p: &Path, with: Option<&str>) {
// Open file // Open file

View File

@@ -51,7 +51,7 @@ impl FileTransferActivity {
} }
fn local_rename_file(&mut self, entry: &File, dest: &Path) { fn local_rename_file(&mut self, entry: &File, dest: &Path) {
match self.host.rename(entry, dest) { match self.host_bridge.rename(entry, dest) {
Ok(_) => { Ok(_) => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,

View File

@@ -93,12 +93,12 @@ impl FileTransferActivity {
} }
fn remote_recv_file(&mut self, opts: TransferOpts) { fn remote_recv_file(&mut self, opts: TransferOpts) {
let wrkdir: PathBuf = self.local().wrkdir.clone(); let wrkdir: PathBuf = self.host_bridge().wrkdir.clone();
match self.get_remote_selected_entries() { match self.get_remote_selected_entries() {
SelectedFile::One(entry) => { SelectedFile::One(entry) => {
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if self.config().get_prompt_on_file_replace() if self.config().get_prompt_on_file_replace()
&& self.local_file_exists(file_to_check.as_path()) && self.host_bridge_file_exists(file_to_check.as_path())
&& !self && !self
.should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name())) .should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name()))
{ {
@@ -129,7 +129,7 @@ impl FileTransferActivity {
let existing_files: Vec<&File> = entries let existing_files: Vec<&File> = entries
.iter() .iter()
.filter(|x| { .filter(|x| {
self.local_file_exists( self.host_bridge_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(), Self::file_to_check_many(x, dest_path.as_path()).as_path(),
) )
}) })

View File

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

View File

@@ -19,7 +19,7 @@ impl FileTransferActivity {
} else if entry.metadata().symlink.is_some() { } else if entry.metadata().symlink.is_some() {
// Stat file // Stat file
let symlink = entry.metadata().symlink.as_ref().unwrap(); let symlink = entry.metadata().symlink.as_ref().unwrap();
let stat_file = match self.host.stat(symlink.as_path()) { let stat_file = match self.host_bridge.stat(symlink.as_path()) {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(err) => {
warn!( warn!(

View File

@@ -9,11 +9,10 @@ use super::{FileTransferActivity, LogLevel, SelectedFile};
impl FileTransferActivity { impl FileTransferActivity {
/// Create symlink on localhost /// Create symlink on localhost
#[cfg(unix)]
pub(crate) fn action_local_symlink(&mut self, name: String) { pub(crate) fn action_local_symlink(&mut self, name: String) {
if let SelectedFile::One(entry) = self.get_local_selected_entries() { if let SelectedFile::One(entry) = self.get_local_selected_entries() {
match self match self
.host .host_bridge
.symlink(PathBuf::from(name.as_str()).as_path(), entry.path()) .symlink(PathBuf::from(name.as_str()).as_path(), entry.path())
{ {
Ok(_) => { Ok(_) => {
@@ -33,11 +32,6 @@ impl FileTransferActivity {
} }
} }
#[cfg(windows)]
pub(crate) fn action_local_symlink(&mut self, _name: String) {
self.mount_error("Symlinks are not supported on Windows hosts");
}
/// Copy file on remote /// Copy file on remote
pub(crate) fn action_remote_symlink(&mut self, name: String) { pub(crate) fn action_remote_symlink(&mut self, name: String) {
if let SelectedFile::One(entry) = self.get_remote_selected_entries() { if let SelectedFile::One(entry) = self.get_remote_selected_entries() {

View File

@@ -18,8 +18,16 @@ impl FileTransferActivity {
pub(crate) fn action_walkdir_local(&mut self) -> Result<Vec<File>, WalkdirError> { pub(crate) fn action_walkdir_local(&mut self) -> Result<Vec<File>, WalkdirError> {
let mut acc = Vec::with_capacity(32_768); let mut acc = Vec::with_capacity(32_768);
self.walkdir(&mut acc, &self.host.pwd(), |activity, path| { let pwd = self
activity.host.list_dir(path).map_err(|e| e.to_string()) .host_bridge
.pwd()
.map_err(|e| WalkdirError::Error(e.to_string()))?;
self.walkdir(&mut acc, &pwd, |activity, path| {
activity
.host_bridge
.list_dir(path)
.map_err(|e| e.to_string())
})?; })?;
Ok(acc) Ok(acc)

View File

@@ -5,7 +5,7 @@
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table}; use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table};
use tuirealm::tui::widgets::{List as TuiList, ListDirection, ListItem, ListState}; use tuirealm::ratatui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue}; use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue};
use super::{Msg, UiMsg}; use super::{Msg, UiMsg};
@@ -32,7 +32,7 @@ impl Log {
} }
impl MockComponent for Log { impl MockComponent for Log {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) { fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::layout::Rect) {
let width: usize = area.width as usize - 4; let width: usize = area.width as usize - 4;
let focus = self let focus = self
.props .props

View File

@@ -16,8 +16,8 @@ use tuirealm::props::{
Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TableBuilder, TextSpan, Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TableBuilder, TextSpan,
}; };
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
#[cfg(unix)] #[cfg(posix)]
use users::{get_group_by_gid, get_user_by_uid}; use uzers::{get_group_by_gid, get_user_by_uid};
pub use self::chmod::ChmodPopup; pub use self::chmod::ChmodPopup;
pub use self::goto::{GotoPopup, ATTR_FILES}; pub use self::goto::{GotoPopup, ATTR_FILES};
@@ -533,7 +533,7 @@ impl FileInfoPopup {
.add_col(TextSpan::from("Last access time: ")) .add_col(TextSpan::from("Last access time: "))
.add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed));
// User // User
#[cfg(unix)] #[cfg(posix)]
let username: String = match file.metadata().uid { let username: String = match file.metadata().uid {
Some(uid) => match get_user_by_uid(uid) { Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(), Some(user) => user.name().to_string_lossy().to_string(),
@@ -541,10 +541,10 @@ impl FileInfoPopup {
}, },
None => String::from("0"), None => String::from("0"),
}; };
#[cfg(windows)] #[cfg(win)]
let username: String = format!("{}", file.metadata().uid.unwrap_or(0)); let username: String = format!("{}", file.metadata().uid.unwrap_or(0));
// Group // Group
#[cfg(unix)] #[cfg(posix)]
let group: String = match file.metadata().gid { let group: String = match file.metadata().gid {
Some(gid) => match get_group_by_gid(gid) { Some(gid) => match get_group_by_gid(gid) {
Some(group) => group.name().to_string_lossy().to_string(), Some(group) => group.name().to_string_lossy().to_string(),
@@ -552,7 +552,7 @@ impl FileInfoPopup {
}, },
None => String::from("0"), None => String::from("0"),
}; };
#[cfg(windows)] #[cfg(win)]
let group: String = format!("{}", file.metadata().gid.unwrap_or(0)); let group: String = format!("{}", file.metadata().gid.unwrap_or(0));
texts texts
.add_row() .add_row()
@@ -1036,7 +1036,7 @@ pub struct ProgressBarFull {
} }
impl ProgressBarFull { impl ProgressBarFull {
pub fn new<S: AsRef<str>>(prog: f64, label: S, title: S, color: Color) -> Self { pub fn new<S: Into<String>>(prog: f64, label: S, title: S, color: Color) -> Self {
Self { Self {
component: ProgressBar::default() component: ProgressBar::default()
.borders( .borders(
@@ -1074,7 +1074,7 @@ pub struct ProgressBarPartial {
} }
impl ProgressBarPartial { impl ProgressBarPartial {
pub fn new<S: AsRef<str>>(prog: f64, label: S, title: S, color: Color) -> Self { pub fn new<S: Into<String>>(prog: f64, label: S, title: S, color: Color) -> Self {
Self { Self {
component: ProgressBar::default() component: ProgressBar::default()
.borders( .borders(
@@ -1551,8 +1551,8 @@ pub struct StatusBarLocal {
impl StatusBarLocal { impl StatusBarLocal {
pub fn new(browser: &Browser, sorting_color: Color, hidden_color: Color) -> Self { pub fn new(browser: &Browser, sorting_color: Color, hidden_color: Color) -> Self {
let file_sorting = file_sorting_label(browser.local().file_sorting); let file_sorting = file_sorting_label(browser.host_bridge().file_sorting);
let hidden_files = hidden_files_label(browser.local().hidden_files_visible()); let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible());
Self { Self {
component: Span::default().spans(&[ component: Span::default().spans(&[
TextSpan::new("File sorting: ").fg(sorting_color), TextSpan::new("File sorting: ").fg(sorting_color),

View File

@@ -3,7 +3,7 @@ use tui_realm_stdlib::Checkbox;
use tuirealm::command::{Cmd, CmdResult, Direction}; use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, AttrValue, Attribute, BorderSides, Borders, Color}; use tuirealm::props::{Alignment, AttrValue, Attribute, BorderSides, Borders, Color};
use tuirealm::tui::layout::{Constraint, Direction as LayoutDirection, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue}; use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue};
use super::{Msg, TransferMsg, UiMsg}; use super::{Msg, TransferMsg, UiMsg};
@@ -186,7 +186,7 @@ impl MockComponent for ChmodPopup {
State::One(StateValue::U32(self.get_mode().into())) State::One(StateValue::U32(self.get_mode().into()))
} }
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) { fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::layout::Rect) {
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) != AttrValue::Flag(true) { if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) != AttrValue::Flag(true) {
return; return;
} }

View File

@@ -198,7 +198,7 @@ impl GotoPopup {
} }
impl MockComponent for GotoPopup { impl MockComponent for GotoPopup {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::prelude::Rect) { fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::prelude::Rect) {
self.input.view(frame, area); self.input.view(frame, area);
} }
@@ -365,7 +365,7 @@ mod test {
} }
#[test] #[test]
#[cfg(unix)] #[cfg(posix)]
fn test_should_suggest_absolute_path() { fn test_should_suggest_absolute_path() {
let mut states = OwnStates { let mut states = OwnStates {
files: vec![ files: vec![

View File

@@ -4,14 +4,15 @@
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::props::{ use tuirealm::props::{
Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers, Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers, TextSpan,
}; };
use tuirealm::tui::text::{Line, Span}; use tuirealm::ratatui::text::{Line, Span};
use tuirealm::tui::widgets::{List as TuiList, ListDirection, ListItem, ListState}; use tuirealm::ratatui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
use tuirealm::{MockComponent, Props, State, StateValue}; use tuirealm::{MockComponent, Props, State, StateValue};
pub const FILE_LIST_CMD_SELECT_ALL: &str = "A"; pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
pub const FILE_LIST_CMD_DESELECT_ALL: &str = "D"; pub const FILE_LIST_CMD_DESELECT_ALL: &str = "D";
const PROP_DOT_DOT: &str = "dot_dot";
/// OwnStates contains states for this component /// OwnStates contains states for this component
#[derive(Clone, Default)] #[derive(Clone, Default)]
@@ -22,8 +23,8 @@ struct OwnStates {
impl OwnStates { impl OwnStates {
/// Initialize list states /// Initialize list states
pub fn init_list_states(&mut self, len: usize) { pub fn init_list_states(&mut self, len: usize, has_dot_dot: bool) {
self.selected = Vec::with_capacity(len); self.selected = Vec::with_capacity(len + if has_dot_dot { 1 } else { 0 });
self.fix_list_index(); self.fix_list_index();
} }
@@ -107,9 +108,9 @@ impl OwnStates {
} }
/// Select all files /// Select all files
pub fn select_all(&mut self) { pub fn select_all(&mut self, has_dot_dot: bool) {
for i in 0..self.list_len() { for i in 0..self.list_len() {
self.select(i); self.select(i + if has_dot_dot { 1 } else { 0 });
} }
} }
@@ -172,10 +173,24 @@ impl FileList {
self.attr(Attribute::Content, AttrValue::Table(rows)); self.attr(Attribute::Content, AttrValue::Table(rows));
self self
} }
/// If enabled, show `..` entry at the beginning of the list
pub fn dot_dot(mut self, show: bool) -> Self {
self.attr(Attribute::Custom(PROP_DOT_DOT), AttrValue::Flag(show));
self
}
/// Returns the value of the `dot_dot` property
fn has_dot_dot(&self) -> bool {
self.props
.get(Attribute::Custom(PROP_DOT_DOT))
.map(|x| x.unwrap_flag())
.unwrap_or(false)
}
} }
impl MockComponent for FileList { impl MockComponent for FileList {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) { fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::layout::Rect) {
let title = self let title = self
.props .props
.get_or( .get_or(
@@ -193,25 +208,42 @@ impl MockComponent for FileList {
.unwrap_flag(); .unwrap_flag();
let div = tui_realm_stdlib::utils::get_block(borders, Some(title), focus, None); let div = tui_realm_stdlib::utils::get_block(borders, Some(title), focus, None);
// Make list entries // Make list entries
let init_table_iter = if self.has_dot_dot() {
vec![vec![TextSpan::from("..")]]
} else {
vec![]
};
let list_items: Vec<ListItem> = match self let list_items: Vec<ListItem> = match self
.props .props
.get(Attribute::Content) .get(Attribute::Content)
.map(|x| x.unwrap_table()) .map(|x| x.unwrap_table())
{ {
Some(table) => table Some(table) => init_table_iter
.iter() .iter()
.chain(table.iter())
.enumerate() .enumerate()
.map(|(num, row)| { .map(|(num, row)| {
let real_num = num;
let num = if self.has_dot_dot() {
num.checked_sub(1).unwrap_or_default()
} else {
num
};
let columns: Vec<Span> = row let columns: Vec<Span> = row
.iter() .iter()
.map(|col| { .map(|col| {
let (fg, bg, mut modifiers) = let (fg, bg, mut modifiers) =
tui_realm_stdlib::utils::use_or_default_styles(&self.props, col); tui_realm_stdlib::utils::use_or_default_styles(&self.props, col);
if self.states.is_selected(num) { if !(self.has_dot_dot() && real_num == 0)
&& self.states.is_selected(num)
{
modifiers |= TextModifiers::REVERSED modifiers |= TextModifiers::REVERSED
| TextModifiers::UNDERLINED | TextModifiers::UNDERLINED
| TextModifiers::ITALIC; | TextModifiers::ITALIC;
} }
Span::styled( Span::styled(
col.content.clone(), col.content.clone(),
Style::default().add_modifier(modifiers).fg(fg).bg(bg), Style::default().add_modifier(modifiers).fg(fg).bg(bg),
@@ -255,6 +287,7 @@ impl MockComponent for FileList {
Some(line) => line.len(), Some(line) => line.len(),
_ => 0, _ => 0,
}, },
self.has_dot_dot(),
); );
self.states.fix_list_index(); self.states.fix_list_index();
} }
@@ -265,8 +298,16 @@ impl MockComponent for FileList {
} }
fn state(&self) -> State { fn state(&self) -> State {
if self.has_dot_dot() && self.states.list_index == 0 {
return State::One(StateValue::String("..".to_string()));
}
match self.states.is_selection_empty() { match self.states.is_selection_empty() {
true => State::One(StateValue::Usize(self.states.list_index())), true => State::One(StateValue::Usize(if self.has_dot_dot() {
self.states.list_index.checked_sub(1).unwrap_or_default()
} else {
self.states.list_index
})),
false => State::Vec( false => State::Vec(
self.states self.states
.get_selection() .get_selection()
@@ -334,7 +375,7 @@ impl MockComponent for FileList {
} }
} }
Cmd::Custom(FILE_LIST_CMD_SELECT_ALL) => { Cmd::Custom(FILE_LIST_CMD_SELECT_ALL) => {
self.states.select_all(); self.states.select_all(self.has_dot_dot());
CmdResult::None CmdResult::None
} }
Cmd::Custom(FILE_LIST_CMD_DESELECT_ALL) => { Cmd::Custom(FILE_LIST_CMD_DESELECT_ALL) => {
@@ -342,7 +383,15 @@ impl MockComponent for FileList {
CmdResult::None CmdResult::None
} }
Cmd::Toggle => { Cmd::Toggle => {
self.states.toggle_file(self.states.list_index()); if self.has_dot_dot() && self.states.list_index() == 0 {
return CmdResult::None;
}
self.states.toggle_file(if self.has_dot_dot() {
self.states.list_index().checked_sub(1).unwrap_or_default()
} else {
self.states.list_index()
});
CmdResult::None CmdResult::None
} }
_ => CmdResult::None, _ => CmdResult::None,

View File

@@ -1,7 +1,7 @@
use tui_realm_stdlib::Input; use tui_realm_stdlib::Input;
use tuirealm::command::{Cmd, CmdResult}; use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Table}; use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Table};
use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::{MockComponent, State}; use tuirealm::{MockComponent, State};
use super::file_list::FileList; use super::file_list::FileList;
@@ -88,7 +88,7 @@ impl FileListWithSearch {
} }
impl MockComponent for FileListWithSearch { impl MockComponent for FileListWithSearch {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) { fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::layout::Rect) {
// split the area in two // split the area in two
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)

View File

@@ -363,7 +363,8 @@ impl ExplorerLocal {
.foreground(fg) .foreground(fg)
.highlighted_color(hg) .highlighted_color(hg)
.title(title, Alignment::Left) .title(title, Alignment::Left)
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()), .rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect())
.dot_dot(true),
} }
} }
} }
@@ -439,11 +440,23 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)), }) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)), }) => {
if matches!(self.component.state(), State::One(StateValue::String(_))) {
Some(Msg::Transfer(TransferMsg::GoToParentDirectory))
} else {
Some(Msg::Transfer(TransferMsg::EnterDirectory))
}
}
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Char(' '), code: Key::Char(' '),
.. ..
}) => Some(Msg::Transfer(TransferMsg::TransferFile)), }) => {
if matches!(self.component.state(), State::One(StateValue::String(_))) {
Some(Msg::None)
} else {
Some(Msg::Transfer(TransferMsg::TransferFile))
}
}
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Char('a'), code: Key::Char('a'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
@@ -559,7 +572,8 @@ impl ExplorerRemote {
.foreground(fg) .foreground(fg)
.highlighted_color(hg) .highlighted_color(hg)
.title(title, Alignment::Left) .title(title, Alignment::Left)
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()), .rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect())
.dot_dot(true),
} }
} }
} }
@@ -635,11 +649,23 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)), }) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)), }) => {
if matches!(self.component.state(), State::One(StateValue::String(_))) {
Some(Msg::Transfer(TransferMsg::GoToParentDirectory))
} else {
Some(Msg::Transfer(TransferMsg::EnterDirectory))
}
}
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Char(' '), code: Key::Char(' '),
.. ..
}) => Some(Msg::Transfer(TransferMsg::TransferFile)), }) => {
if matches!(self.component.state(), State::One(StateValue::String(_))) {
Some(Msg::None)
} else {
Some(Msg::Transfer(TransferMsg::TransferFile))
}
}
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Char('a'), code: Key::Char('a'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,

View File

@@ -30,10 +30,10 @@ impl FileTransferActivity {
Ok(Some(FsChange::Update(update))) => { Ok(Some(FsChange::Update(update))) => {
debug!( debug!(
"fs watcher reported an `Update` from {} to {}", "fs watcher reported an `Update` from {} to {}",
update.local().display(), update.host_bridge().display(),
update.remote().display() update.remote().display()
); );
self.upload_watched_file(update.local(), update.remote()); self.upload_watched_file(update.host_bridge(), update.remote());
} }
Err(err) => { Err(err) => {
self.log( self.log(
@@ -87,9 +87,9 @@ impl FileTransferActivity {
} }
} }
fn upload_watched_file(&mut self, local: &Path, remote: &Path) { fn upload_watched_file(&mut self, host: &Path, remote: &Path) {
// stat local file // stat host file
let entry = match self.host.stat(local) { let entry = match self.host_bridge.stat(host) {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(err) => {
self.log( self.log(
@@ -105,8 +105,8 @@ impl FileTransferActivity {
}; };
// send // send
trace!( trace!(
"syncing local file {} with remote {}", "syncing host file {} with remote {}",
local.display(), host.display(),
remote.display() remote.display()
); );
let remote_path = remote.parent().unwrap_or_else(|| Path::new("/")); let remote_path = remote.parent().unwrap_or_else(|| Path::new("/"));
@@ -116,7 +116,7 @@ impl FileTransferActivity {
LogLevel::Info, LogLevel::Info,
format!( format!(
"synched watched file {} with {}", "synched watched file {} with {}",
local.display(), host.display(),
remote.display() remote.display()
), ),
); );

View File

@@ -16,10 +16,10 @@ const FUZZY_SEARCH_THRESHOLD: u16 = 50;
/// File explorer tab /// File explorer tab
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub enum FileExplorerTab { pub enum FileExplorerTab {
Local, HostBridge,
Remote, Remote,
FindLocal, // Find result tab FindHostBridge, // Find result tab
FindRemote, // Find result tab FindRemote, // Find result tab
} }
/// Describes the explorer tab type /// Describes the explorer tab type
@@ -31,10 +31,10 @@ pub enum FoundExplorerTab {
/// Browser contains the browser options /// Browser contains the browser options
pub struct Browser { pub struct Browser {
local: FileExplorer, // Local File explorer state host_bridge: FileExplorer, // Local File explorer state
remote: FileExplorer, // Remote File explorer state remote: FileExplorer, // Remote File explorer state
found: Option<Found>, // File explorer for find result found: Option<Found>, // File explorer for find result
tab: FileExplorerTab, // Current selected tab tab: FileExplorerTab, // Current selected tab
pub sync_browsing: bool, pub sync_browsing: bool,
} }
@@ -42,30 +42,30 @@ impl Browser {
/// Build a new `Browser` struct /// Build a new `Browser` struct
pub fn new(cli: &ConfigClient) -> Self { pub fn new(cli: &ConfigClient) -> Self {
Self { Self {
local: Self::build_local_explorer(cli), host_bridge: Self::build_local_explorer(cli),
remote: Self::build_remote_explorer(cli), remote: Self::build_remote_explorer(cli),
found: None, found: None,
tab: FileExplorerTab::Local, tab: FileExplorerTab::HostBridge,
sync_browsing: false, sync_browsing: false,
} }
} }
pub fn explorer(&self) -> &FileExplorer { pub fn explorer(&self) -> &FileExplorer {
match self.tab { match self.tab {
FileExplorerTab::Local => &self.local, FileExplorerTab::HostBridge => &self.host_bridge,
FileExplorerTab::Remote => &self.remote, FileExplorerTab::Remote => &self.remote,
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
self.found.as_ref().map(|x| &x.explorer).unwrap() self.found.as_ref().map(|x| &x.explorer).unwrap()
} }
} }
} }
pub fn local(&self) -> &FileExplorer { pub fn host_bridge(&self) -> &FileExplorer {
&self.local &self.host_bridge
} }
pub fn local_mut(&mut self) -> &mut FileExplorer { pub fn host_bridge_mut(&mut self) -> &mut FileExplorer {
&mut self.local &mut self.host_bridge
} }
pub fn remote(&self) -> &FileExplorer { pub fn remote(&self) -> &FileExplorer {

View File

@@ -1,8 +1,6 @@
// Locals
use std::env; use std::env;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
// Ext
use bytesize::ByteSize; use bytesize::ByteSize;
use tuirealm::props::{ use tuirealm::props::{
Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextSpan, Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextSpan,
@@ -11,7 +9,7 @@ use tuirealm::{PollStrategy, Update};
use super::browser::FileExplorerTab; use super::browser::FileExplorerTab;
use super::{ConfigClient, FileTransferActivity, Id, LogLevel, LogRecord, TransferPayload}; use super::{ConfigClient, FileTransferActivity, Id, LogLevel, LogRecord, TransferPayload};
use crate::filetransfer::ProtocolParams; use crate::filetransfer::{HostBridgeParams, ProtocolParams};
use crate::system::environment; use crate::system::environment;
use crate::system::notifications::Notification; use crate::system::notifications::Notification;
use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex}; use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex};
@@ -95,9 +93,9 @@ impl FileTransferActivity {
env::set_var("EDITOR", self.config().get_text_editor()); env::set_var("EDITOR", self.config().get_text_editor());
} }
/// Convert a path to absolute according to local explorer /// Convert a path to absolute according to host explorer
pub(super) fn local_to_abs_path(&self, path: &Path) -> PathBuf { pub(super) fn host_bridge_to_abs_path(&self, path: &Path) -> PathBuf {
path::absolutize(self.local().wrkdir.as_path(), path) path::absolutize(self.host_bridge().wrkdir.as_path(), path)
} }
/// Convert a path to absolute according to remote explorer /// Convert a path to absolute according to remote explorer
@@ -107,8 +105,28 @@ impl FileTransferActivity {
/// Get remote hostname /// Get remote hostname
pub(super) fn get_remote_hostname(&self) -> String { pub(super) fn get_remote_hostname(&self) -> String {
let ft_params = self.context().ft_params().unwrap(); let ft_params = self.context().remote_params().unwrap();
match &ft_params.params { self.get_hostname(&ft_params.params)
}
pub(super) fn get_hostbridge_hostname(&self) -> String {
let host_bridge_params = self.context().host_bridge_params().unwrap();
match host_bridge_params {
HostBridgeParams::Localhost(_) => {
let hostname = match hostname::get() {
Ok(h) => h,
Err(_) => return String::from("localhost"),
};
let hostname: String = hostname.as_os_str().to_string_lossy().to_string();
let tokens: Vec<&str> = hostname.split('.').collect();
String::from(*tokens.first().unwrap_or(&"localhost"))
}
HostBridgeParams::Remote(_, params) => self.get_hostname(params),
}
}
fn get_hostname(&self, params: &ProtocolParams) -> String {
match params {
ProtocolParams::Generic(params) => params.address.clone(), ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::AwsS3(params) => params.bucket_name.clone(), ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
ProtocolParams::Kube(params) => { ProtocolParams::Kube(params) => {
@@ -217,9 +235,9 @@ impl FileTransferActivity {
} }
} }
/// Update local file list /// Update host bridge file list
pub(super) fn update_local_filelist(&mut self) { pub(super) fn update_host_bridge_filelist(&mut self) {
self.reload_local_dir(); self.reload_host_bridge_dir();
// Get width // Get width
let width = self let width = self
.context_mut() .context_mut()
@@ -228,29 +246,26 @@ impl FileTransferActivity {
.size() .size()
.map(|x| (x.width / 2) - 2) .map(|x| (x.width / 2) - 2)
.unwrap_or(0) as usize; .unwrap_or(0) as usize;
let hostname: String = match hostname::get() { let hostname = self.get_hostbridge_hostname();
Ok(h) => {
let hostname: String = h.as_os_str().to_string_lossy().to_string();
let tokens: Vec<&str> = hostname.split('.').collect();
String::from(*tokens.first().unwrap_or(&"localhost"))
}
Err(_) => String::from("localhost"),
};
let hostname: String = format!( let hostname: String = format!(
"{}:{} ", "{hostname}:{} ",
hostname, fmt_path_elide_ex(
fmt_path_elide_ex(self.local().wrkdir.as_path(), width, hostname.len() + 3) // 3 because of '/…/' self.host_bridge().wrkdir.as_path(),
width,
hostname.len() + 3
) // 3 because of '/…/'
); );
let files: Vec<Vec<TextSpan>> = self let files: Vec<Vec<TextSpan>> = self
.local() .host_bridge()
.iter_files() .iter_files()
.map(|x| vec![TextSpan::from(self.local().fmt_file(x))]) .map(|x| vec![TextSpan::from(self.host_bridge().fmt_file(x))])
.collect(); .collect();
// Update content and title // Update content and title
assert!(self assert!(self
.app .app
.attr( .attr(
&Id::ExplorerLocal, &Id::ExplorerHostBridge,
Attribute::Content, Attribute::Content,
AttrValue::Table(files) AttrValue::Table(files)
) )
@@ -258,7 +273,7 @@ impl FileTransferActivity {
assert!(self assert!(self
.app .app
.attr( .attr(
&Id::ExplorerLocal, &Id::ExplorerHostBridge,
Attribute::Title, Attribute::Title,
AttrValue::Title((hostname, Alignment::Left)) AttrValue::Title((hostname, Alignment::Left))
) )
@@ -409,17 +424,19 @@ impl FileTransferActivity {
self.browser.del_found(); self.browser.del_found();
// Restore tab // Restore tab
let new_tab = match self.browser.tab() { let new_tab = match self.browser.tab() {
FileExplorerTab::FindLocal => FileExplorerTab::Local, FileExplorerTab::FindHostBridge => FileExplorerTab::HostBridge,
FileExplorerTab::FindRemote => FileExplorerTab::Remote, FileExplorerTab::FindRemote => FileExplorerTab::Remote,
_ => FileExplorerTab::Local, _ => FileExplorerTab::HostBridge,
}; };
// Give focus to new tab // Give focus to new tab
match new_tab { match new_tab {
FileExplorerTab::Local => assert!(self.app.active(&Id::ExplorerLocal).is_ok()), FileExplorerTab::HostBridge => {
assert!(self.app.active(&Id::ExplorerHostBridge).is_ok())
}
FileExplorerTab::Remote => { FileExplorerTab::Remote => {
assert!(self.app.active(&Id::ExplorerRemote).is_ok()) assert!(self.app.active(&Id::ExplorerRemote).is_ok())
} }
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
assert!(self.app.active(&Id::ExplorerFind).is_ok()) assert!(self.app.active(&Id::ExplorerFind).is_ok())
} }
} }
@@ -445,15 +462,21 @@ impl FileTransferActivity {
pub(super) fn update_browser_file_list(&mut self) { pub(super) fn update_browser_file_list(&mut self) {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_local_filelist(), FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
self.update_host_bridge_filelist()
}
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(), FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(),
} }
} }
pub(super) fn update_browser_file_list_swapped(&mut self) { pub(super) fn update_browser_file_list_swapped(&mut self) {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_remote_filelist(), FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_local_filelist(), self.update_remote_filelist()
}
FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
self.update_host_bridge_filelist()
}
} }
} }
} }

View File

@@ -28,11 +28,13 @@ use session::TransferPayload;
use tempfile::TempDir; use tempfile::TempDir;
use tuirealm::{Application, EventListenerCfg, NoUserEvent}; use tuirealm::{Application, EventListenerCfg, NoUserEvent};
use super::{Activity, Context, ExitReason}; use super::{Activity, Context, ExitReason, CROSSTERM_MAX_POLL};
use crate::config::themes::Theme; use crate::config::themes::Theme;
use crate::explorer::{FileExplorer, FileSorting}; use crate::explorer::{FileExplorer, FileSorting};
use crate::filetransfer::{Builder, FileTransferParams}; use crate::filetransfer::{
use crate::host::Localhost; FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder,
};
use crate::host::HostBridge;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
use crate::system::watcher::FsWatcher; use crate::system::watcher::FsWatcher;
@@ -47,7 +49,7 @@ enum Id {
ErrorPopup, ErrorPopup,
ExecPopup, ExecPopup,
ExplorerFind, ExplorerFind,
ExplorerLocal, ExplorerHostBridge,
ExplorerRemote, ExplorerRemote,
FatalPopup, FatalPopup,
FileInfoPopup, FileInfoPopup,
@@ -68,7 +70,7 @@ enum Id {
ReplacingFilesListPopup, ReplacingFilesListPopup,
SaveAsPopup, SaveAsPopup,
SortingPopup, SortingPopup,
StatusBarLocal, StatusBarHostBridge,
StatusBarRemote, StatusBarRemote,
SymlinkPopup, SymlinkPopup,
SyncBrowsingMkdirPopup, SyncBrowsingMkdirPopup,
@@ -213,8 +215,8 @@ pub struct FileTransferActivity {
app: Application<Id, Msg, NoUserEvent>, app: Application<Id, Msg, NoUserEvent>,
/// Whether should redraw UI /// Whether should redraw UI
redraw: bool, redraw: bool,
/// Localhost bridge /// Host bridge
host: Localhost, host_bridge: Box<dyn HostBridge>,
/// Remote host client /// Remote host client
client: Box<dyn RemoteFs>, client: Box<dyn RemoteFs>,
/// Browser /// Browser
@@ -229,26 +231,40 @@ pub struct FileTransferActivity {
cache: Option<TempDir>, cache: Option<TempDir>,
/// Fs watcher /// Fs watcher
fswatcher: Option<FsWatcher>, fswatcher: Option<FsWatcher>,
/// connected once /// host bridge connected
connected: bool, host_bridge_connected: bool,
/// remote connected once
remote_connected: bool,
} }
impl FileTransferActivity { impl FileTransferActivity {
/// Instantiates a new FileTransferActivity /// Instantiates a new FileTransferActivity
pub fn new(host: Localhost, params: &FileTransferParams, ticks: Duration) -> Self { pub fn new(
host_bridge_params: HostBridgeParams,
remote_params: &FileTransferParams,
ticks: Duration,
) -> Self {
// Get config client // Get config client
let config_client: ConfigClient = Self::init_config_client(); let config_client: ConfigClient = Self::init_config_client();
// init host bridge
let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client);
let host_bridge_connected = host_bridge.is_localhost();
let enable_fs_watcher = host_bridge.is_localhost();
Self { Self {
exit_reason: None, exit_reason: None,
context: None, context: None,
app: Application::init( app: Application::init(
EventListenerCfg::default() EventListenerCfg::default()
.poll_timeout(ticks) .poll_timeout(ticks)
.default_input_listener(ticks), .crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
), ),
redraw: true, redraw: true,
host, host_bridge,
client: Builder::build(params.protocol, params.params.clone(), &config_client), client: RemoteFsBuilder::build(
remote_params.protocol,
remote_params.params.clone(),
&config_client,
),
browser: Browser::new(&config_client), browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
walkdir: WalkdirStates::default(), walkdir: WalkdirStates::default(),
@@ -257,23 +273,22 @@ impl FileTransferActivity {
Ok(d) => Some(d), Ok(d) => Some(d),
Err(_) => None, Err(_) => None,
}, },
fswatcher: match FsWatcher::init(Duration::from_secs(5)) { fswatcher: if enable_fs_watcher {
Ok(w) => Some(w), FsWatcher::init(Duration::from_secs(5)).ok()
Err(e) => { } else {
error!("failed to initialize fs watcher: {}", e); None
None
}
}, },
connected: false, host_bridge_connected,
remote_connected: false,
} }
} }
fn local(&self) -> &FileExplorer { fn host_bridge(&self) -> &FileExplorer {
self.browser.local() self.browser.host_bridge()
} }
fn local_mut(&mut self) -> &mut FileExplorer { fn host_bridge_mut(&mut self) -> &mut FileExplorer {
self.browser.local_mut() self.browser.host_bridge_mut()
} }
fn remote(&self) -> &FileExplorer { fn remote(&self) -> &FileExplorer {
@@ -361,7 +376,10 @@ impl Activity for FileTransferActivity {
error!("Failed to enter raw mode: {}", err); error!("Failed to enter raw mode: {}", err);
} }
// Get files at current pwd // Get files at current pwd
self.reload_local_dir(); if self.host_bridge.is_localhost() {
debug!("Reloading host bridge directory");
self.reload_host_bridge_dir();
}
debug!("Read working directory"); debug!("Read working directory");
// Configure text editor // Configure text editor
self.setup_text_editor(); self.setup_text_editor();
@@ -384,15 +402,34 @@ impl Activity for FileTransferActivity {
if self.context.is_none() { if self.context.is_none() {
return; return;
} }
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error) // Check if connected to host bridge (popup must be None, otherwise would try reconnecting in loop in case of error)
if (!self.client.is_connected() || !self.connected) && !self.app.mounted(&Id::FatalPopup) { if (!self.host_bridge.is_connected() || !self.host_bridge_connected)
let ftparams = self.context().ft_params().unwrap(); && !self.app.mounted(&Id::FatalPopup)
&& !self.host_bridge.is_localhost()
{
let host_bridge_params = self.context().host_bridge_params().unwrap();
let ft_params = host_bridge_params.unwrap_protocol_params();
// print params
let msg: String = Self::get_connection_msg(ft_params);
// Set init state to connecting popup
self.mount_blocking_wait(msg.as_str());
// Connect to remote
self.connect_to_host_bridge();
// Redraw
self.redraw = true;
}
// Check if connected to remote (popup must be None, otherwise would try reconnecting in loop in case of error)
if (!self.client.is_connected() || !self.remote_connected)
&& !self.app.mounted(&Id::FatalPopup)
&& self.host_bridge.is_connected()
{
let ftparams = self.context().remote_params().unwrap();
// print params // print params
let msg: String = Self::get_connection_msg(&ftparams.params); let msg: String = Self::get_connection_msg(&ftparams.params);
// Set init state to connecting popup // Set init state to connecting popup
self.mount_blocking_wait(msg.as_str()); self.mount_blocking_wait(msg.as_str());
// Connect to remote // Connect to remote
self.connect(); self.connect_to_remote();
// Redraw // Redraw
self.redraw = true; self.redraw = true;
} }
@@ -432,6 +469,10 @@ impl Activity for FileTransferActivity {
if self.client.is_connected() { if self.client.is_connected() {
let _ = self.client.disconnect(); let _ = self.client.disconnect();
} }
// disconnect host bridge
if self.host_bridge.is_connected() {
let _ = self.host_bridge.disconnect();
}
self.context.take() self.context.take()
} }
} }

View File

@@ -2,13 +2,10 @@
//! //!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
// Locals use std::io::{Read, Write};
use std::fs::File as StdFile;
use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Instant; use std::time::Instant;
// Ext
use bytesize::ByteSize; use bytesize::ByteSize;
use remotefs::fs::{File, Metadata, ReadStream, UnixPex, Welcome, WriteStream}; use remotefs::fs::{File, Metadata, ReadStream, UnixPex, Welcome, WriteStream};
use remotefs::{RemoteError, RemoteErrorType, RemoteResult}; use remotefs::{RemoteError, RemoteErrorType, RemoteResult};
@@ -26,10 +23,8 @@ const BUFSIZE: usize = 65535;
enum TransferErrorReason { enum TransferErrorReason {
#[error("File transfer aborted")] #[error("File transfer aborted")]
Abrupted, Abrupted,
#[error("Failed to seek file: {0}")] #[error("I/O error on host_bridgehost: {0}")]
CouldNotRewind(std::io::Error), HostIoError(std::io::Error),
#[error("I/O error on localhost: {0}")]
LocalIoError(std::io::Error),
#[error("Host error: {0}")] #[error("Host error: {0}")]
HostError(HostError), HostError(HostError),
#[error("I/O error on remote: {0}")] #[error("I/O error on remote: {0}")]
@@ -50,15 +45,57 @@ pub(super) enum TransferPayload {
} }
impl FileTransferActivity { impl FileTransferActivity {
pub(super) fn connect_to_host_bridge(&mut self) {
let ft_params = self.context().remote_params().unwrap().clone();
let entry_dir: Option<PathBuf> = ft_params.local_path;
// Connect to host bridge
match self.host_bridge.connect() {
Ok(()) => {
self.host_bridge_connected = self.host_bridge.is_connected();
if !self.host_bridge_connected {
return;
}
// Log welcome
self.log(
LogLevel::Info,
format!(
"Established connection with '{}'",
self.get_hostbridge_hostname()
),
);
// Try to change directory to entry directory
let mut remote_chdir: Option<PathBuf> = None;
if let Some(remote_path) = &entry_dir {
remote_chdir = Some(remote_path.clone());
}
if let Some(remote_path) = remote_chdir {
self.local_changedir(remote_path.as_path(), false);
}
// Set state to explorer
self.umount_wait();
self.reload_host_bridge_dir();
// Update file lists
self.update_host_bridge_filelist();
}
Err(err) => {
// Set popup fatal error
self.umount_wait();
self.mount_fatal(err.to_string());
}
}
}
/// Connect to remote /// Connect to remote
pub(super) fn connect(&mut self) { pub(super) fn connect_to_remote(&mut self) {
let ft_params = self.context().ft_params().unwrap().clone(); let ft_params = self.context().remote_params().unwrap().clone();
let entry_dir: Option<PathBuf> = ft_params.remote_path; let entry_dir: Option<PathBuf> = ft_params.remote_path;
// Connect to remote // Connect to remote
match self.client.connect() { match self.client.connect() {
Ok(Welcome { banner, .. }) => { Ok(Welcome { banner, .. }) => {
self.connected = self.client.is_connected(); self.remote_connected = self.client.is_connected();
if !self.connected { if !self.remote_connected {
return; return;
} }
@@ -94,7 +131,7 @@ impl FileTransferActivity {
self.umount_wait(); self.umount_wait();
self.reload_remote_dir(); self.reload_remote_dir();
// Update file lists // Update file lists
self.update_local_filelist(); self.update_host_bridge_filelist();
self.update_remote_filelist(); self.update_remote_filelist();
} }
Err(err) => { Err(err) => {
@@ -124,7 +161,7 @@ impl FileTransferActivity {
/// Reload remote directory entries and update browser /// Reload remote directory entries and update browser
pub(super) fn reload_remote_dir(&mut self) { pub(super) fn reload_remote_dir(&mut self) {
if !self.connected { if !self.remote_connected {
return; return;
} }
// Get current entries // Get current entries
@@ -149,35 +186,48 @@ impl FileTransferActivity {
} }
} }
/// Reload local directory entries and update browser /// Reload host_bridge directory entries and update browser
pub(super) fn reload_local_dir(&mut self) { pub(super) fn reload_host_bridge_dir(&mut self) {
self.mount_blocking_wait("Loading local directory..."); if !self.host_bridge_connected {
return;
}
let wrkdir: PathBuf = self.host.pwd(); self.mount_blocking_wait("Loading host bridge directory...");
let res = self.local_scan(wrkdir.as_path()); let wrkdir = match self.host_bridge.pwd() {
Ok(wrkdir) => wrkdir,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not scan current host bridge directory: {err}"),
);
return;
}
};
let res = self.host_bridge_scan(wrkdir.as_path());
self.umount_wait(); self.umount_wait();
match res { match res {
Ok(_) => { Ok(_) => {
self.local_mut().wrkdir = wrkdir; self.host_bridge_mut().wrkdir = wrkdir;
} }
Err(err) => { Err(err) => {
self.log_and_alert( self.log_and_alert(
LogLevel::Error, LogLevel::Error,
format!("Could not scan current local directory: {err}"), format!("Could not scan current host bridge directory: {err}"),
); );
} }
} }
} }
/// Scan current local directory /// Scan current host bridge directory
fn local_scan(&mut self, path: &Path) -> Result<(), HostError> { fn host_bridge_scan(&mut self, path: &Path) -> Result<(), HostError> {
match self.host.list_dir(path) { match self.host_bridge.list_dir(path) {
Ok(files) => { Ok(files) => {
// Set files and sort (sorting is implicit) // Set files and sort (sorting is implicit)
self.local_mut().set_files(files); self.host_bridge_mut().set_files(files);
Ok(()) Ok(())
} }
@@ -270,7 +320,7 @@ impl FileTransferActivity {
// Reset states // Reset states
self.transfer.reset(); self.transfer.reset();
// Calculate total size of transfer // Calculate total size of transfer
let total_transfer_size: usize = self.get_total_transfer_size_local(entry); let total_transfer_size: usize = self.get_total_transfer_size_host(entry);
self.transfer.full.init(total_transfer_size); self.transfer.full.init(total_transfer_size);
// Mount progress bar // Mount progress bar
self.mount_progress_bar(format!("Uploading {}", entry.path().display())); self.mount_progress_bar(format!("Uploading {}", entry.path().display()));
@@ -292,7 +342,7 @@ impl FileTransferActivity {
// Calculate total size of transfer // Calculate total size of transfer
let total_transfer_size: usize = entries let total_transfer_size: usize = entries
.iter() .iter()
.map(|x| self.get_total_transfer_size_local(x)) .map(|x| self.get_total_transfer_size_host(x))
.sum(); .sum();
self.transfer.full.init(total_transfer_size); self.transfer.full.init(total_transfer_size);
// Mount progress bar // Mount progress bar
@@ -358,7 +408,7 @@ impl FileTransferActivity {
} }
} }
// Get files in dir // Get files in dir
match self.host.list_dir(entry.path()) { match self.host_bridge.list_dir(entry.path()) {
Ok(entries) => { Ok(entries) => {
// Iterate over files // Iterate over files
for entry in entries.iter() { for entry in entries.iter() {
@@ -433,17 +483,17 @@ impl FileTransferActivity {
result result
} }
/// Send local file and write it to remote path /// Send host_bridge file and write it to remote path
fn filetransfer_send_one( fn filetransfer_send_one(
&mut self, &mut self,
local: &File, host_bridge: &File,
remote: &Path, remote: &Path,
file_name: String, file_name: String,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// Sync file size and attributes before transfer // Sync file size and attributes before transfer
let metadata = self let metadata = self
.host .host_bridge
.stat(local.path.as_path()) .stat(host_bridge.path.as_path())
.map_err(TransferErrorReason::HostError) .map_err(TransferErrorReason::HostError)
.map(|x| x.metadata().clone())?; .map(|x| x.metadata().clone())?;
@@ -452,22 +502,30 @@ impl FileTransferActivity {
LogLevel::Info, LogLevel::Info,
format!( format!(
"file {} won't be transferred since hasn't changed", "file {} won't be transferred since hasn't changed",
local.path().display() host_bridge.path().display()
), ),
); );
self.transfer.full.update_progress(metadata.size as usize); self.transfer.full.update_progress(metadata.size as usize);
return Ok(()); return Ok(());
} }
// Upload file // Upload file
// Try to open local file // Try to open host_bridge file
match self.host.open_file_read(local.path.as_path()) { match self.host_bridge.open_file(host_bridge.path.as_path()) {
Ok(fhnd) => match self.client.create(remote, &metadata) { Ok(host_bridge_read) => match self.client.create(remote, &metadata) {
Ok(rhnd) => { Ok(rhnd) => self.filetransfer_send_one_with_stream(
self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd) host_bridge,
} remote,
Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => { file_name,
self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd) host_bridge_read,
} rhnd,
),
Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => self
.filetransfer_send_one_wno_stream(
host_bridge,
remote,
file_name,
host_bridge_read,
),
Err(err) => Err(TransferErrorReason::FileTransferError(err)), Err(err) => Err(TransferErrorReason::FileTransferError(err)),
}, },
Err(err) => Err(TransferErrorReason::HostError(err)), Err(err) => Err(TransferErrorReason::HostError(err)),
@@ -477,20 +535,21 @@ impl FileTransferActivity {
/// Send file to remote using stream /// Send file to remote using stream
fn filetransfer_send_one_with_stream( fn filetransfer_send_one_with_stream(
&mut self, &mut self,
local: &File, host: &File,
remote: &Path, remote: &Path,
file_name: String, file_name: String,
mut reader: StdFile, mut reader: Box<dyn Read + Send>,
mut writer: WriteStream, mut writer: WriteStream,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// Write file // Write file
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; let file_size = self
.host_bridge
.stat(host.path())
.map_err(TransferErrorReason::HostError)
.map(|x| x.metadata().size as usize)?;
// Init transfer // Init transfer
self.transfer.partial.init(file_size); self.transfer.partial.init(file_size);
// rewind
if let Err(err) = reader.rewind() {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Write remote file // Write remote file
let mut total_bytes_written: usize = 0; let mut total_bytes_written: usize = 0;
let mut last_progress_val: f64 = 0.0; let mut last_progress_val: f64 = 0.0;
@@ -535,7 +594,7 @@ impl FileTransferActivity {
} }
} }
Err(err) => { Err(err) => {
return Err(TransferErrorReason::LocalIoError(err)); return Err(TransferErrorReason::HostIoError(err));
} }
}; };
// Increase progress // Increase progress
@@ -561,14 +620,14 @@ impl FileTransferActivity {
return Err(TransferErrorReason::Abrupted); return Err(TransferErrorReason::Abrupted);
} }
// set stat // set stat
if let Err(err) = self.client.setstat(remote, local.metadata().clone()) { if let Err(err) = self.client.setstat(remote, host.metadata().clone()) {
error!("failed to set stat for {}: {}", remote.display(), err); error!("failed to set stat for {}: {}", remote.display(), err);
} }
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!( format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.path.display(), host.path.display(),
remote.display(), remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()), fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()), ByteSize(self.transfer.partial.calc_bytes_per_second()),
@@ -580,30 +639,31 @@ impl FileTransferActivity {
/// Send an `File` to remote without using streams. /// Send an `File` to remote without using streams.
fn filetransfer_send_one_wno_stream( fn filetransfer_send_one_wno_stream(
&mut self, &mut self,
local: &File, host: &File,
remote: &Path, remote: &Path,
file_name: String, file_name: String,
mut reader: StdFile, reader: Box<dyn Read + Send>,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// Sync file size and attributes before transfer // Sync file size and attributes before transfer
let metadata = self let metadata = self
.host .host_bridge
.stat(local.path.as_path()) .stat(host.path.as_path())
.map_err(TransferErrorReason::HostError) .map_err(TransferErrorReason::HostError)
.map(|x| x.metadata().clone())?; .map(|x| x.metadata().clone())?;
// Write file // Write file
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; let file_size = self
.host_bridge
.stat(host.path())
.map_err(TransferErrorReason::HostError)
.map(|x| x.metadata().size as usize)?;
// Init transfer // Init transfer
self.transfer.partial.init(file_size); self.transfer.partial.init(file_size);
// rewind
if let Err(err) = reader.rewind() {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Draw before // Draw before
self.update_progress_bar(format!("Uploading \"{file_name}\"")); self.update_progress_bar(format!("Uploading \"{file_name}\""));
self.view(); self.view();
// Send file // Send file
if let Err(err) = self.client.create_file(remote, &metadata, Box::new(reader)) { if let Err(err) = self.client.create_file(remote, &metadata, reader) {
return Err(TransferErrorReason::FileTransferError(err)); return Err(TransferErrorReason::FileTransferError(err));
} }
// set stat // set stat
@@ -621,7 +681,7 @@ impl FileTransferActivity {
LogLevel::Info, LogLevel::Info,
format!( format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.path.display(), host.path.display(),
remote.display(), remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()), fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()), ByteSize(self.transfer.partial.calc_bytes_per_second()),
@@ -636,15 +696,17 @@ impl FileTransferActivity {
pub(super) fn filetransfer_recv( pub(super) fn filetransfer_recv(
&mut self, &mut self,
payload: TransferPayload, payload: TransferPayload,
local_path: &Path, host_bridge_path: &Path,
dst_name: Option<String>, dst_name: Option<String>,
) -> Result<(), String> { ) -> Result<(), String> {
let result = match payload { let result = match payload {
TransferPayload::Any(ref entry) => { TransferPayload::Any(ref entry) => {
self.filetransfer_recv_any(entry, local_path, dst_name) self.filetransfer_recv_any(entry, host_bridge_path, dst_name)
}
TransferPayload::File(ref file) => self.filetransfer_recv_file(file, host_bridge_path),
TransferPayload::Many(ref entries) => {
self.filetransfer_recv_many(entries, host_bridge_path)
} }
TransferPayload::File(ref file) => self.filetransfer_recv_file(file, local_path),
TransferPayload::Many(ref entries) => self.filetransfer_recv_many(entries, local_path),
}; };
// Notify // Notify
match &result { match &result {
@@ -664,7 +726,7 @@ impl FileTransferActivity {
fn filetransfer_recv_any( fn filetransfer_recv_any(
&mut self, &mut self,
entry: &File, entry: &File,
local_path: &Path, host_path: &Path,
dst_name: Option<String>, dst_name: Option<String>,
) -> Result<(), String> { ) -> Result<(), String> {
// Reset states // Reset states
@@ -675,14 +737,18 @@ impl FileTransferActivity {
// Mount progress bar // Mount progress bar
self.mount_progress_bar(format!("Downloading {}", entry.path().display())); self.mount_progress_bar(format!("Downloading {}", entry.path().display()));
// Receive // Receive
let result = self.filetransfer_recv_recurse(entry, local_path, dst_name); let result = self.filetransfer_recv_recurse(entry, host_path, dst_name);
// Umount progress bar // Umount progress bar
self.umount_progress_bar(); self.umount_progress_bar();
result result
} }
/// Receive a single file from remote. /// Receive a single file from remote.
fn filetransfer_recv_file(&mut self, entry: &File, local_path: &Path) -> Result<(), String> { fn filetransfer_recv_file(
&mut self,
entry: &File,
host_bridge_path: &Path,
) -> Result<(), String> {
// Reset states // Reset states
self.transfer.reset(); self.transfer.reset();
// Calculate total transfer size // Calculate total transfer size
@@ -691,7 +757,7 @@ impl FileTransferActivity {
// Mount progress bar // Mount progress bar
self.mount_progress_bar(format!("Downloading {}", entry.path.display())); self.mount_progress_bar(format!("Downloading {}", entry.path.display()));
// Receive // Receive
let result = self.filetransfer_recv_one(local_path, entry, entry.name()); let result = self.filetransfer_recv_one(host_bridge_path, entry, entry.name());
// Umount progress bar // Umount progress bar
self.umount_progress_bar(); self.umount_progress_bar();
// Return result // Return result
@@ -728,7 +794,7 @@ impl FileTransferActivity {
fn filetransfer_recv_recurse( fn filetransfer_recv_recurse(
&mut self, &mut self,
entry: &File, entry: &File,
local_path: &Path, host_bridge_path: &Path,
dst_name: Option<String>, dst_name: Option<String>,
) -> Result<(), String> { ) -> Result<(), String> {
// Write popup // Write popup
@@ -736,32 +802,35 @@ impl FileTransferActivity {
// Match entry // Match entry
let result: Result<(), String> = if entry.is_dir() { let result: Result<(), String> = if entry.is_dir() {
// Get dir name // Get dir name
let mut local_dir_path: PathBuf = PathBuf::from(local_path); let mut host_bridge_dir_path: PathBuf = PathBuf::from(host_bridge_path);
match dst_name { match dst_name {
Some(name) => local_dir_path.push(name), Some(name) => host_bridge_dir_path.push(name),
None => local_dir_path.push(entry.name()), None => host_bridge_dir_path.push(entry.name()),
} }
// Create directory on local // Create directory on host_bridge
match self.host.mkdir_ex(local_dir_path.as_path(), true) { match self
.host_bridge
.mkdir_ex(host_bridge_dir_path.as_path(), true)
{
Ok(_) => { Ok(_) => {
// Apply file mode to directory // Apply file mode to directory
if let Err(err) = self if let Err(err) = self
.host .host_bridge
.setstat(local_dir_path.as_path(), entry.metadata()) .setstat(host_bridge_dir_path.as_path(), entry.metadata())
{ {
self.log( self.log(
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not set stat to directory {:?} to \"{}\": {}", "Could not set stat to directory {:?} to \"{}\": {}",
entry.metadata(), entry.metadata(),
local_dir_path.display(), host_bridge_dir_path.display(),
err err
), ),
); );
} }
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!("Created directory \"{}\"", local_dir_path.display()), format!("Created directory \"{}\"", host_bridge_dir_path.display()),
); );
// Get files in dir // Get files in dir
match self.client.list_dir(entry.path()) { match self.client.list_dir(entry.path()) {
@@ -773,10 +842,10 @@ impl FileTransferActivity {
break; break;
} }
// Receive entry; name is always None after first call // Receive entry; name is always None after first call
// Local path becomes local_dir_path // Local path becomes host_bridge_dir_path
self.filetransfer_recv_recurse( self.filetransfer_recv_recurse(
entry, entry,
local_dir_path.as_path(), host_bridge_dir_path.as_path(),
None, None,
)? )?
} }
@@ -800,7 +869,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!( format!(
"Failed to create directory \"{}\": {}", "Failed to create directory \"{}\": {}",
local_dir_path.display(), host_bridge_dir_path.display(),
err err
), ),
); );
@@ -808,39 +877,39 @@ impl FileTransferActivity {
} }
} }
} else { } else {
// Get local file // Get host_bridge file
let mut local_file_path: PathBuf = PathBuf::from(local_path); let mut host_bridge_file_path: PathBuf = PathBuf::from(host_bridge_path);
let local_file_name: String = match dst_name { let host_bridge_file_name: String = match dst_name {
Some(n) => n, Some(n) => n,
None => entry.name(), None => entry.name(),
}; };
local_file_path.push(local_file_name.as_str()); host_bridge_file_path.push(host_bridge_file_name.as_str());
// Download file // Download file
if let Err(err) = if let Err(err) =
self.filetransfer_recv_one(local_file_path.as_path(), entry, file_name) self.filetransfer_recv_one(host_bridge_file_path.as_path(), entry, file_name)
{ {
// If transfer was abrupted or there was an IO error on remote, remove file // If transfer was abrupted or there was an IO error on remote, remove file
if matches!( if matches!(
err, err,
TransferErrorReason::Abrupted | TransferErrorReason::LocalIoError(_) TransferErrorReason::Abrupted | TransferErrorReason::HostIoError(_)
) { ) {
// Stat file // Stat file
match self.host.stat(local_file_path.as_path()) { match self.host_bridge.stat(host_bridge_file_path.as_path()) {
Err(err) => self.log( Err(err) => self.log(
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not remove created file {}: {}", "Could not remove created file {}: {}",
local_file_path.display(), host_bridge_file_path.display(),
err err
), ),
), ),
Ok(entry) => { Ok(entry) => {
if let Err(err) = self.host.remove(&entry) { if let Err(err) = self.host_bridge.remove(&entry) {
self.log( self.log(
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not remove created file {}: {}", "Could not remove created file {}: {}",
local_file_path.display(), host_bridge_file_path.display(),
err err
), ),
); );
@@ -853,8 +922,8 @@ impl FileTransferActivity {
Ok(()) Ok(())
} }
}; };
// Reload directory on local // Reload directory on host_bridge
self.reload_local_dir(); self.reload_host_bridge_dir();
// if aborted; show alert // if aborted; show alert
if self.transfer.aborted() { if self.transfer.aborted() {
// Log abort // Log abort
@@ -866,15 +935,15 @@ impl FileTransferActivity {
result result
} }
/// Receive file from remote and write it to local path /// Receive file from remote and write it to host_bridge path
fn filetransfer_recv_one( fn filetransfer_recv_one(
&mut self, &mut self,
local: &Path, host_bridge: &Path,
remote: &File, remote: &File,
file_name: String, file_name: String,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// check if files are equal (in case, don't transfer) // check if files are equal (in case, don't transfer)
if !self.has_local_file_changed(local, remote) { if !self.has_host_bridge_file_changed(host_bridge, remote) {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!( format!(
@@ -888,16 +957,20 @@ impl FileTransferActivity {
return Ok(()); return Ok(());
} }
// Try to open local file // Try to open host_bridge file
match self.host.open_file_write(local) { match self.host_bridge.create_file(host_bridge, &remote.metadata) {
Ok(local_file) => { Ok(writer) => {
// Download file from remote // Download file from remote
match self.client.open(remote.path.as_path()) { match self.client.open(remote.path.as_path()) {
Ok(rhnd) => self.filetransfer_recv_one_with_stream( Ok(rhnd) => self.filetransfer_recv_one_with_stream(
local, remote, file_name, rhnd, local_file, host_bridge,
remote,
file_name,
rhnd,
writer,
), ),
Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => { Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => {
self.filetransfer_recv_one_wno_stream(local, remote, file_name) self.filetransfer_recv_one_wno_stream(host_bridge, remote, file_name)
} }
Err(err) => Err(TransferErrorReason::FileTransferError(err)), Err(err) => Err(TransferErrorReason::FileTransferError(err)),
} }
@@ -909,16 +982,16 @@ impl FileTransferActivity {
/// Receive an `File` from remote using stream /// Receive an `File` from remote using stream
fn filetransfer_recv_one_with_stream( fn filetransfer_recv_one_with_stream(
&mut self, &mut self,
local: &Path, host_bridge: &Path,
remote: &File, remote: &File,
file_name: String, file_name: String,
mut reader: ReadStream, mut reader: ReadStream,
mut writer: StdFile, mut writer: Box<dyn Write + Send>,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
let mut total_bytes_written: usize = 0; let mut total_bytes_written: usize = 0;
// Init transfer // Init transfer
self.transfer.partial.init(remote.metadata.size as usize); self.transfer.partial.init(remote.metadata.size as usize);
// Write local file // Write host_bridge file
let mut last_progress_val: f64 = 0.0; let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None; let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely read, // While the entire file hasn't been completely read,
@@ -951,7 +1024,7 @@ impl FileTransferActivity {
match writer.write(&buffer[delta..bytes_read]) { match writer.write(&buffer[delta..bytes_read]) {
Ok(bytes) => delta += bytes, Ok(bytes) => delta += bytes,
Err(err) => { Err(err) => {
return Err(TransferErrorReason::LocalIoError(err)); return Err(TransferErrorReason::HostIoError(err));
} }
} }
} }
@@ -984,14 +1057,20 @@ impl FileTransferActivity {
if self.transfer.aborted() { if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted); return Err(TransferErrorReason::Abrupted);
} }
// finalize write
self.host_bridge
.finalize_write(writer)
.map_err(TransferErrorReason::HostError)?;
// Apply file mode to file // Apply file mode to file
if let Err(err) = self.host.setstat(local, remote.metadata()) { if let Err(err) = self.host_bridge.setstat(host_bridge, remote.metadata()) {
self.log( self.log(
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not set stat to file {:?} to \"{}\": {}", "Could not set stat to file {:?} to \"{}\": {}",
remote.metadata(), remote.metadata(),
local.display(), host_bridge.display(),
err err
), ),
); );
@@ -1002,25 +1081,26 @@ impl FileTransferActivity {
format!( format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.path.display(), remote.path.display(),
local.display(), host_bridge.display(),
fmt_millis(self.transfer.partial.started().elapsed()), fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()), ByteSize(self.transfer.partial.calc_bytes_per_second()),
), ),
); );
Ok(()) Ok(())
} }
/// Receive an `File` from remote without using stream /// Receive an `File` from remote without using stream
fn filetransfer_recv_one_wno_stream( fn filetransfer_recv_one_wno_stream(
&mut self, &mut self,
local: &Path, host_bridge: &Path,
remote: &File, remote: &File,
file_name: String, file_name: String,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// Open local file // Open host_bridge file
let reader = self let reader = self
.host .host_bridge
.open_file_write(local) .create_file(host_bridge, &remote.metadata)
.map_err(TransferErrorReason::HostError) .map_err(TransferErrorReason::HostError)
.map(Box::new)?; .map(Box::new)?;
// Init transfer // Init transfer
@@ -1043,13 +1123,13 @@ impl FileTransferActivity {
self.update_progress_bar(format!("Downloading \"{file_name}\"")); self.update_progress_bar(format!("Downloading \"{file_name}\""));
self.view(); self.view();
// Apply file mode to file // Apply file mode to file
if let Err(err) = self.host.setstat(local, remote.metadata()) { if let Err(err) = self.host_bridge.setstat(host_bridge, remote.metadata()) {
self.log( self.log(
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not set stat to file {:?} to \"{}\": {}", "Could not set stat to file {:?} to \"{}\": {}",
remote.metadata(), remote.metadata(),
local.display(), host_bridge.display(),
err err
), ),
); );
@@ -1060,7 +1140,7 @@ impl FileTransferActivity {
format!( format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.path.display(), remote.path.display(),
local.display(), host_bridge.display(),
fmt_millis(self.transfer.partial.started().elapsed()), fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()), ByteSize(self.transfer.partial.calc_bytes_per_second()),
), ),
@@ -1068,20 +1148,47 @@ impl FileTransferActivity {
Ok(()) Ok(())
} }
/// Change directory for local /// Change directory for host_bridge
pub(super) fn local_changedir(&mut self, path: &Path, push: bool) { pub(super) fn host_bridge_changedir(&mut self, path: &Path, push: bool) {
// Get current directory // Get current directory
let prev_dir: PathBuf = self.local().wrkdir.clone(); let prev_dir: PathBuf = self.host_bridge().wrkdir.clone();
// Change directory // Change directory
match self.host.change_wrkdir(path) { match self.host_bridge.change_wrkdir(path) {
Ok(_) => { Ok(_) => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!("Changed directory on local: {}", path.display()), format!("Changed directory on host_bridge: {}", path.display()),
); );
// Push prev_dir to stack // Push prev_dir to stack
if push { if push {
self.local_mut().pushd(prev_dir.as_path()) self.host_bridge_mut().pushd(prev_dir.as_path())
}
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not change working directory: {err}"),
);
}
}
}
pub(super) fn local_changedir(&mut self, path: &Path, push: bool) {
// Get current directory
let prev_dir: PathBuf = self.host_bridge().wrkdir.clone();
// Change directory
match self.host_bridge.change_wrkdir(path) {
Ok(_) => {
self.log(
LogLevel::Info,
format!("Changed directory on host bridge: {}", path.display()),
);
// Update files
self.reload_host_bridge_dir();
// Push prev_dir to stack
if push {
self.host_bridge_mut().pushd(prev_dir.as_path())
} }
} }
Err(err) => { Err(err) => {
@@ -1152,14 +1259,14 @@ impl FileTransferActivity {
// -- transfer sizes // -- transfer sizes
/// Get total size of transfer for localhost /// Get total size of transfer for host_bridgehost
fn get_total_transfer_size_local(&mut self, entry: &File) -> usize { fn get_total_transfer_size_host(&mut self, entry: &File) -> usize {
if entry.is_dir() { if entry.is_dir() {
// List dir // List dir
match self.host.list_dir(entry.path()) { match self.host_bridge.list_dir(entry.path()) {
Ok(files) => files Ok(files) => files
.iter() .iter()
.map(|x| self.get_total_transfer_size_local(x)) .map(|x| self.get_total_transfer_size_host(x))
.sum(), .sum(),
Err(err) => { Err(err) => {
self.log( self.log(
@@ -1206,23 +1313,23 @@ impl FileTransferActivity {
// file changed // file changed
/// Check whether provided file has changed on local disk, compared to remote file /// Check whether provided file has changed on host_bridge disk, compared to remote file
fn has_local_file_changed(&self, local: &Path, remote: &File) -> bool { fn has_host_bridge_file_changed(&mut self, host_bridge: &Path, remote: &File) -> bool {
// check if files are equal (in case, don't transfer) // check if files are equal (in case, don't transfer)
if let Ok(local_file) = self.host.stat(local) { if let Ok(host_bridge_file) = self.host_bridge.stat(host_bridge) {
local_file.metadata().modified != remote.metadata().modified host_bridge_file.metadata().modified != remote.metadata().modified
|| local_file.metadata().size != remote.metadata().size || host_bridge_file.metadata().size != remote.metadata().size
} else { } else {
true true
} }
} }
/// Checks whether remote file has changed compared to local file /// Checks whether remote file has changed compared to host_bridge file
fn has_remote_file_changed(&mut self, remote: &Path, local_metadata: &Metadata) -> bool { fn has_remote_file_changed(&mut self, remote: &Path, host_bridge_metadata: &Metadata) -> bool {
// check if files are equal (in case, don't transfer) // check if files are equal (in case, don't transfer)
if let Ok(remote_file) = self.client.stat(remote) { if let Ok(remote_file) = self.client.stat(remote) {
local_metadata.modified != remote_file.metadata().modified host_bridge_metadata.modified != remote_file.metadata().modified
|| local_metadata.size != remote_file.metadata().size || host_bridge_metadata.size != remote_file.metadata().size
} else { } else {
true true
} }
@@ -1230,11 +1337,11 @@ impl FileTransferActivity {
// -- file exist // -- file exist
pub(crate) fn local_file_exists(&mut self, p: &Path) -> bool { pub(crate) fn host_bridge_file_exists(&mut self, p: &Path) -> bool {
self.host.file_exists(p) self.host_bridge.exists(p).unwrap_or_default()
} }
pub(crate) fn remote_file_exists(&mut self, p: &Path) -> bool { pub(crate) fn remote_file_exists(&mut self, p: &Path) -> bool {
self.client.stat(p).is_ok() self.client.exists(p).unwrap_or_default()
} }
} }

View File

@@ -40,14 +40,12 @@ impl FileTransferActivity {
self.umount_chmod(); self.umount_chmod();
self.mount_blocking_wait("Applying new file mode…"); self.mount_blocking_wait("Applying new file mode…");
match self.browser.tab() { match self.browser.tab() {
#[cfg(unix)] FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge
FileExplorerTab::Local => self.action_local_chmod(mode), if self.host_bridge.is_localhost() && cfg!(windows) => {}
#[cfg(unix)] FileExplorerTab::HostBridge => self.action_local_chmod(mode),
FileExplorerTab::FindLocal => self.action_find_local_chmod(mode), FileExplorerTab::FindHostBridge => self.action_find_local_chmod(mode),
FileExplorerTab::Remote => self.action_remote_chmod(mode), FileExplorerTab::Remote => self.action_remote_chmod(mode),
FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode), FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode),
#[cfg(windows)]
FileExplorerTab::Local | FileExplorerTab::FindLocal => {}
} }
self.umount_wait(); self.umount_wait();
self.update_browser_file_list(); self.update_browser_file_list();
@@ -56,7 +54,7 @@ impl FileTransferActivity {
self.umount_copy(); self.umount_copy();
self.mount_blocking_wait("Copying file(s)…"); self.mount_blocking_wait("Copying file(s)…");
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_copy(dest), FileExplorerTab::HostBridge => self.action_local_copy(dest),
FileExplorerTab::Remote => self.action_remote_copy(dest), FileExplorerTab::Remote => self.action_remote_copy(dest),
_ => panic!("Found tab doesn't support COPY"), _ => panic!("Found tab doesn't support COPY"),
} }
@@ -68,7 +66,7 @@ impl FileTransferActivity {
self.umount_symlink(); self.umount_symlink();
self.mount_blocking_wait("Creating symlink…"); self.mount_blocking_wait("Creating symlink…");
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_symlink(name), FileExplorerTab::HostBridge => self.action_local_symlink(name),
FileExplorerTab::Remote => self.action_remote_symlink(name), FileExplorerTab::Remote => self.action_remote_symlink(name),
_ => panic!("Found tab doesn't support SYMLINK"), _ => panic!("Found tab doesn't support SYMLINK"),
} }
@@ -80,9 +78,9 @@ impl FileTransferActivity {
self.umount_radio_delete(); self.umount_radio_delete();
self.mount_blocking_wait("Removing file(s)…"); self.mount_blocking_wait("Removing file(s)…");
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_delete(), FileExplorerTab::HostBridge => self.action_local_delete(),
FileExplorerTab::Remote => self.action_remote_delete(), FileExplorerTab::Remote => self.action_remote_delete(),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
// Get entry // Get entry
self.action_find_delete(); self.action_find_delete();
// Delete entries // Delete entries
@@ -108,20 +106,20 @@ impl FileTransferActivity {
self.umount_wait(); self.umount_wait();
// Reload files // Reload files
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(), FileExplorerTab::HostBridge => self.update_host_bridge_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(), FileExplorerTab::Remote => self.update_remote_filelist(),
FileExplorerTab::FindLocal => self.update_local_filelist(), FileExplorerTab::FindHostBridge => self.update_host_bridge_filelist(),
FileExplorerTab::FindRemote => self.update_remote_filelist(), FileExplorerTab::FindRemote => self.update_remote_filelist(),
} }
} }
TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Local => { TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::HostBridge => {
if let SelectedFile::One(entry) = self.get_local_selected_entries() { if let SelectedFile::One(entry) = self.get_local_selected_entries() {
self.action_submit_local(entry); self.action_submit_local(entry);
// Update file list if sync // Update file list if sync
if self.browser.sync_browsing && self.browser.found().is_none() { if self.browser.sync_browsing && self.browser.found().is_none() {
self.update_remote_filelist(); self.update_remote_filelist();
} }
self.update_local_filelist(); self.update_host_bridge_filelist();
} }
} }
TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => { TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => {
@@ -129,7 +127,7 @@ impl FileTransferActivity {
self.action_submit_remote(entry); self.action_submit_remote(entry);
// Update file list if sync // Update file list if sync
if self.browser.sync_browsing && self.browser.found().is_none() { if self.browser.sync_browsing && self.browser.found().is_none() {
self.update_local_filelist(); self.update_host_bridge_filelist();
} }
self.update_remote_filelist(); self.update_remote_filelist();
} }
@@ -150,7 +148,7 @@ impl FileTransferActivity {
self.umount_exec(); self.umount_exec();
self.mount_blocking_wait(format!("Executing '{cmd}'…").as_str()); self.mount_blocking_wait(format!("Executing '{cmd}'…").as_str());
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_exec(cmd), FileExplorerTab::HostBridge => self.action_local_exec(cmd),
FileExplorerTab::Remote => self.action_remote_exec(cmd), FileExplorerTab::Remote => self.action_remote_exec(cmd),
_ => panic!("Found tab doesn't support EXEC"), _ => panic!("Found tab doesn't support EXEC"),
} }
@@ -160,7 +158,7 @@ impl FileTransferActivity {
} }
TransferMsg::GoTo(dir) => { TransferMsg::GoTo(dir) => {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_change_local_dir(dir), FileExplorerTab::HostBridge => self.action_change_local_dir(dir),
FileExplorerTab::Remote => self.action_change_remote_dir(dir), FileExplorerTab::Remote => self.action_change_remote_dir(dir),
_ => panic!("Found tab doesn't support GOTO"), _ => panic!("Found tab doesn't support GOTO"),
} }
@@ -175,18 +173,18 @@ impl FileTransferActivity {
} }
TransferMsg::GoToParentDirectory => { TransferMsg::GoToParentDirectory => {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => { FileExplorerTab::HostBridge => {
self.action_go_to_local_upper_dir(); self.action_go_to_local_upper_dir();
if self.browser.sync_browsing && self.browser.found().is_none() { if self.browser.sync_browsing && self.browser.found().is_none() {
self.update_remote_filelist(); self.update_remote_filelist();
} }
// Reload file list component // Reload file list component
self.update_local_filelist() self.update_host_bridge_filelist()
} }
FileExplorerTab::Remote => { FileExplorerTab::Remote => {
self.action_go_to_remote_upper_dir(); self.action_go_to_remote_upper_dir();
if self.browser.sync_browsing && self.browser.found().is_none() { if self.browser.sync_browsing && self.browser.found().is_none() {
self.update_local_filelist(); self.update_host_bridge_filelist();
} }
// Reload file list component // Reload file list component
self.update_remote_filelist() self.update_remote_filelist()
@@ -196,18 +194,18 @@ impl FileTransferActivity {
} }
TransferMsg::GoToPreviousDirectory => { TransferMsg::GoToPreviousDirectory => {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => { FileExplorerTab::HostBridge => {
self.action_go_to_previous_local_dir(); self.action_go_to_previous_local_dir();
if self.browser.sync_browsing && self.browser.found().is_none() { if self.browser.sync_browsing && self.browser.found().is_none() {
self.update_remote_filelist(); self.update_remote_filelist();
} }
// Reload file list component // Reload file list component
self.update_local_filelist() self.update_host_bridge_filelist()
} }
FileExplorerTab::Remote => { FileExplorerTab::Remote => {
self.action_go_to_previous_remote_dir(); self.action_go_to_previous_remote_dir();
if self.browser.sync_browsing && self.browser.found().is_none() { if self.browser.sync_browsing && self.browser.found().is_none() {
self.update_local_filelist(); self.update_host_bridge_filelist();
} }
// Reload file list component // Reload file list component
self.update_remote_filelist() self.update_remote_filelist()
@@ -220,7 +218,7 @@ impl FileTransferActivity {
self.mount_walkdir_wait(); self.mount_walkdir_wait();
// Find // Find
let res: Result<Vec<File>, WalkdirError> = match self.browser.tab() { let res: Result<Vec<File>, WalkdirError> = match self.browser.tab() {
FileExplorerTab::Local => self.action_walkdir_local(), FileExplorerTab::HostBridge => self.action_walkdir_local(),
FileExplorerTab::Remote => self.action_walkdir_remote(), FileExplorerTab::Remote => self.action_walkdir_remote(),
_ => panic!("Trying to search for files, while already in a find result"), _ => panic!("Trying to search for files, while already in a find result"),
}; };
@@ -242,13 +240,13 @@ impl FileTransferActivity {
Ok(files) => { Ok(files) => {
// Get wrkdir // Get wrkdir
let wrkdir = match self.browser.tab() { let wrkdir = match self.browser.tab() {
FileExplorerTab::Local => self.local().wrkdir.clone(), FileExplorerTab::HostBridge => self.host_bridge().wrkdir.clone(),
_ => self.remote().wrkdir.clone(), _ => self.remote().wrkdir.clone(),
}; };
// Create explorer and load files // Create explorer and load files
self.browser.set_found( self.browser.set_found(
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => FoundExplorerTab::Local, FileExplorerTab::HostBridge => FoundExplorerTab::Local,
_ => FoundExplorerTab::Remote, _ => FoundExplorerTab::Remote,
}, },
files, files,
@@ -261,16 +259,16 @@ impl FileTransferActivity {
self.update_find_list(); self.update_find_list();
// Initialize tab // Initialize tab
self.browser.change_tab(match self.browser.tab() { self.browser.change_tab(match self.browser.tab() {
FileExplorerTab::Local => FileExplorerTab::FindLocal, FileExplorerTab::HostBridge => FileExplorerTab::FindHostBridge,
FileExplorerTab::Remote => FileExplorerTab::FindRemote, FileExplorerTab::Remote => FileExplorerTab::FindRemote,
_ => FileExplorerTab::FindLocal, _ => FileExplorerTab::FindHostBridge,
}); });
} }
} }
} }
TransferMsg::Mkdir(dir) => { TransferMsg::Mkdir(dir) => {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_mkdir(dir), FileExplorerTab::HostBridge => self.action_local_mkdir(dir),
FileExplorerTab::Remote => self.action_remote_mkdir(dir), FileExplorerTab::Remote => self.action_remote_mkdir(dir),
_ => {} _ => {}
} }
@@ -280,7 +278,7 @@ impl FileTransferActivity {
} }
TransferMsg::NewFile(name) => { TransferMsg::NewFile(name) => {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_newfile(name), FileExplorerTab::HostBridge => self.action_local_newfile(name),
FileExplorerTab::Remote => self.action_remote_newfile(name), FileExplorerTab::Remote => self.action_remote_newfile(name),
_ => {} _ => {}
} }
@@ -289,15 +287,17 @@ impl FileTransferActivity {
self.update_browser_file_list() self.update_browser_file_list()
} }
TransferMsg::OpenFile => match self.browser.tab() { TransferMsg::OpenFile => match self.browser.tab() {
FileExplorerTab::Local => self.action_open_local(), FileExplorerTab::HostBridge => self.action_open_local(),
FileExplorerTab::Remote => self.action_open_remote(), FileExplorerTab::Remote => self.action_open_remote(),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => self.action_find_open(), FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
self.action_find_open()
}
}, },
TransferMsg::OpenFileWith(prog) => { TransferMsg::OpenFileWith(prog) => {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_open_with(&prog), FileExplorerTab::HostBridge => self.action_local_open_with(&prog),
FileExplorerTab::Remote => self.action_remote_open_with(&prog), FileExplorerTab::Remote => self.action_remote_open_with(&prog),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
self.action_find_open_with(&prog) self.action_find_open_with(&prog)
} }
} }
@@ -305,7 +305,7 @@ impl FileTransferActivity {
} }
TransferMsg::OpenTextFile => { TransferMsg::OpenTextFile => {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_edit_local_file(), FileExplorerTab::HostBridge => self.action_edit_local_file(),
FileExplorerTab::Remote => self.action_edit_remote_file(), FileExplorerTab::Remote => self.action_edit_remote_file(),
_ => {} _ => {}
} }
@@ -316,7 +316,7 @@ impl FileTransferActivity {
self.umount_rename(); self.umount_rename();
self.mount_blocking_wait("Moving file(s)…"); self.mount_blocking_wait("Moving file(s)…");
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_rename(dest), FileExplorerTab::HostBridge => self.action_local_rename(dest),
FileExplorerTab::Remote => self.action_remote_rename(dest), FileExplorerTab::Remote => self.action_remote_rename(dest),
_ => {} _ => {}
} }
@@ -336,9 +336,9 @@ impl FileTransferActivity {
TransferMsg::SaveFileAs(dest) => { TransferMsg::SaveFileAs(dest) => {
self.umount_saveas(); self.umount_saveas();
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_saveas(dest), FileExplorerTab::HostBridge => self.action_local_saveas(dest),
FileExplorerTab::Remote => self.action_remote_saveas(dest), FileExplorerTab::Remote => self.action_remote_saveas(dest),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
// Get entry // Get entry
self.action_find_transfer(TransferOpts::default().save_as(Some(dest))); self.action_find_transfer(TransferOpts::default().save_as(Some(dest)));
} }
@@ -352,9 +352,9 @@ impl FileTransferActivity {
TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index), TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index),
TransferMsg::TransferFile => { TransferMsg::TransferFile => {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => self.action_local_send(), FileExplorerTab::HostBridge => self.action_local_send(),
FileExplorerTab::Remote => self.action_remote_recv(), FileExplorerTab::Remote => self.action_remote_recv(),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
self.action_find_transfer(TransferOpts::default()) self.action_find_transfer(TransferOpts::default())
} }
} }
@@ -371,8 +371,8 @@ impl FileTransferActivity {
UiMsg::CloseChmodPopup => self.umount_chmod(), UiMsg::CloseChmodPopup => self.umount_chmod(),
UiMsg::ChangeFileSorting(sorting) => { UiMsg::ChangeFileSorting(sorting) => {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => { FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
self.local_mut().sort_by(sorting); self.host_bridge_mut().sort_by(sorting);
self.refresh_local_status_bar(); self.refresh_local_status_bar();
} }
FileExplorerTab::Remote | FileExplorerTab::FindRemote => { FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
@@ -384,22 +384,28 @@ impl FileTransferActivity {
} }
UiMsg::ChangeTransferWindow => { UiMsg::ChangeTransferWindow => {
let new_tab = match self.browser.tab() { let new_tab = match self.browser.tab() {
FileExplorerTab::Local if self.browser.found().is_some() => { FileExplorerTab::HostBridge if self.browser.found().is_some() => {
FileExplorerTab::FindRemote FileExplorerTab::FindRemote
} }
FileExplorerTab::FindLocal | FileExplorerTab::Local => FileExplorerTab::Remote, FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
FileExplorerTab::Remote if self.browser.found().is_some() => { FileExplorerTab::Remote
FileExplorerTab::FindLocal }
FileExplorerTab::Remote if self.browser.found().is_some() => {
FileExplorerTab::FindHostBridge
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
FileExplorerTab::HostBridge
} }
FileExplorerTab::FindRemote | FileExplorerTab::Remote => FileExplorerTab::Local,
}; };
// Set focus // Set focus
match new_tab { match new_tab {
FileExplorerTab::Local => assert!(self.app.active(&Id::ExplorerLocal).is_ok()), FileExplorerTab::HostBridge => {
assert!(self.app.active(&Id::ExplorerHostBridge).is_ok())
}
FileExplorerTab::Remote => { FileExplorerTab::Remote => {
assert!(self.app.active(&Id::ExplorerRemote).is_ok()) assert!(self.app.active(&Id::ExplorerRemote).is_ok())
} }
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
assert!(self.app.active(&Id::ExplorerFind).is_ok()) assert!(self.app.active(&Id::ExplorerFind).is_ok())
} }
} }
@@ -441,13 +447,13 @@ impl FileTransferActivity {
let files = self.filter(&filter); let files = self.filter(&filter);
// Get wrkdir // Get wrkdir
let wrkdir = match self.browser.tab() { let wrkdir = match self.browser.tab() {
FileExplorerTab::Local => self.local().wrkdir.clone(), FileExplorerTab::HostBridge => self.host_bridge().wrkdir.clone(),
_ => self.remote().wrkdir.clone(), _ => self.remote().wrkdir.clone(),
}; };
// Create explorer and load files // Create explorer and load files
self.browser.set_found( self.browser.set_found(
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::Local => FoundExplorerTab::Local, FileExplorerTab::HostBridge => FoundExplorerTab::Local,
_ => FoundExplorerTab::Remote, _ => FoundExplorerTab::Remote,
}, },
files, files,
@@ -458,9 +464,9 @@ impl FileTransferActivity {
self.update_find_list(); self.update_find_list();
// Initialize tab // Initialize tab
self.browser.change_tab(match self.browser.tab() { self.browser.change_tab(match self.browser.tab() {
FileExplorerTab::Local => FileExplorerTab::FindLocal, FileExplorerTab::HostBridge => FileExplorerTab::FindHostBridge,
FileExplorerTab::Remote => FileExplorerTab::FindRemote, FileExplorerTab::Remote => FileExplorerTab::FindRemote,
_ => FileExplorerTab::FindLocal, _ => FileExplorerTab::FindHostBridge,
}); });
} }
UiMsg::FuzzySearch(needle) => { UiMsg::FuzzySearch(needle) => {
@@ -471,7 +477,7 @@ impl FileTransferActivity {
assert!(self.app.active(&Id::Log).is_ok()); assert!(self.app.active(&Id::Log).is_ok());
} }
UiMsg::LogBackTabbed => { UiMsg::LogBackTabbed => {
assert!(self.app.active(&Id::ExplorerLocal).is_ok()); assert!(self.app.active(&Id::ExplorerHostBridge).is_ok());
} }
UiMsg::Quit => { UiMsg::Quit => {
self.disconnect_and_quit(); self.disconnect_and_quit();
@@ -488,14 +494,16 @@ impl FileTransferActivity {
} }
UiMsg::ShowChmodPopup => { UiMsg::ShowChmodPopup => {
let selected_file = match self.browser.tab() { let selected_file = match self.browser.tab() {
#[cfg(unix)] #[cfg(posix)]
FileExplorerTab::Local => self.get_local_selected_entries(), FileExplorerTab::HostBridge => self.get_local_selected_entries(),
#[cfg(unix)] #[cfg(posix)]
FileExplorerTab::FindLocal => self.get_found_selected_entries(), FileExplorerTab::FindHostBridge => self.get_found_selected_entries(),
FileExplorerTab::Remote => self.get_remote_selected_entries(), FileExplorerTab::Remote => self.get_remote_selected_entries(),
FileExplorerTab::FindRemote => self.get_found_selected_entries(), FileExplorerTab::FindRemote => self.get_found_selected_entries(),
#[cfg(windows)] #[cfg(win)]
FileExplorerTab::Local | FileExplorerTab::FindLocal => SelectedFile::None, FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
SelectedFile::None
}
}; };
if let Some(mode) = selected_file.unix_pex() { if let Some(mode) = selected_file.unix_pex() {
self.mount_chmod( self.mount_chmod(
@@ -516,7 +524,7 @@ impl FileTransferActivity {
UiMsg::ShowDeletePopup => self.mount_radio_delete(), UiMsg::ShowDeletePopup => self.mount_radio_delete(),
UiMsg::ShowDisconnectPopup => self.mount_disconnect(), UiMsg::ShowDisconnectPopup => self.mount_disconnect(),
UiMsg::ShowExecPopup => self.mount_exec(), UiMsg::ShowExecPopup => self.mount_exec(),
UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::Local => { UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::HostBridge => {
if let SelectedFile::One(file) = self.get_local_selected_entries() { if let SelectedFile::One(file) = self.get_local_selected_entries() {
self.mount_file_info(&file); self.mount_file_info(&file);
} }
@@ -543,9 +551,9 @@ impl FileTransferActivity {
UiMsg::ShowSaveAsPopup => self.mount_saveas(), UiMsg::ShowSaveAsPopup => self.mount_saveas(),
UiMsg::ShowSymlinkPopup => { UiMsg::ShowSymlinkPopup => {
if match self.browser.tab() { if match self.browser.tab() {
FileExplorerTab::Local => self.is_local_selected_one(), FileExplorerTab::HostBridge => self.is_local_selected_one(),
FileExplorerTab::Remote => self.is_remote_selected_one(), FileExplorerTab::Remote => self.is_remote_selected_one(),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => false, FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => false,
} { } {
// Only if only one entry is selected // Only if only one entry is selected
self.mount_symlink(); self.mount_symlink();
@@ -558,8 +566,8 @@ impl FileTransferActivity {
UiMsg::ShowWatchedPathsList => self.action_show_watched_paths_list(), UiMsg::ShowWatchedPathsList => self.action_show_watched_paths_list(),
UiMsg::ShowWatcherPopup => self.action_show_radio_watch(), UiMsg::ShowWatcherPopup => self.action_show_radio_watch(),
UiMsg::ToggleHiddenFiles => match self.browser.tab() { UiMsg::ToggleHiddenFiles => match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => { FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
self.browser.local_mut().toggle_hidden_files(); self.browser.host_bridge_mut().toggle_hidden_files();
self.refresh_local_status_bar(); self.refresh_local_status_bar();
self.update_browser_file_list(); self.update_browser_file_list();
} }

View File

@@ -7,8 +7,8 @@
use remotefs::fs::{File, UnixPex}; use remotefs::fs::{File, UnixPex};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{PropPayload, PropValue, TextSpan}; use tuirealm::props::{PropPayload, PropValue, TextSpan};
use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::tui::widgets::Clear; use tuirealm::ratatui::widgets::Clear;
use tuirealm::{AttrValue, Attribute, Sub, SubClause, SubEventClause}; use tuirealm::{AttrValue, Attribute, Sub, SubClause, SubEventClause};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@@ -44,7 +44,7 @@ impl FileTransferActivity {
assert!(self assert!(self
.app .app
.mount( .mount(
Id::ExplorerLocal, Id::ExplorerHostBridge,
Box::new(components::ExplorerLocal::new( Box::new(components::ExplorerLocal::new(
"", "",
&[], &[],
@@ -81,12 +81,12 @@ impl FileTransferActivity {
self.refresh_local_status_bar(); self.refresh_local_status_bar();
self.refresh_remote_status_bar(); self.refresh_remote_status_bar();
// Update components // Update components
self.update_local_filelist(); self.update_host_bridge_filelist();
// self.update_remote_filelist(); // self.update_remote_filelist();
// Global listener // Global listener
self.mount_global_listener(); self.mount_global_listener();
// Give focus to local explorer // Give focus to local explorer
assert!(self.app.active(&Id::ExplorerLocal).is_ok()); assert!(self.app.active(&Id::ExplorerHostBridge).is_ok());
} }
// -- view // -- view
@@ -106,7 +106,7 @@ impl FileTransferActivity {
] ]
.as_ref(), .as_ref(),
) )
.split(f.size()); .split(f.area());
// main chunks // main chunks
let main_chunks = Layout::default() let main_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
@@ -141,7 +141,7 @@ impl FileTransferActivity {
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) { if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) {
self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]); self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]);
} else { } else {
self.app.view(&Id::ExplorerLocal, f, tabs_chunks[0]); self.app.view(&Id::ExplorerHostBridge, f, tabs_chunks[0]);
} }
// @! Remote explorer (Find or default) // @! Remote explorer (Find or default)
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) { if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) {
@@ -152,80 +152,81 @@ impl FileTransferActivity {
// Draw log box // Draw log box
self.app.view(&Id::Log, f, bottom_chunks[1]); self.app.view(&Id::Log, f, bottom_chunks[1]);
// Draw status bar // Draw status bar
self.app.view(&Id::StatusBarLocal, f, status_bar_chunks[0]); self.app
.view(&Id::StatusBarHostBridge, f, status_bar_chunks[0]);
self.app.view(&Id::StatusBarRemote, f, status_bar_chunks[1]); self.app.view(&Id::StatusBarRemote, f, status_bar_chunks[1]);
// @! Draw popups // @! Draw popups
if self.app.mounted(&Id::FatalPopup) { if self.app.mounted(&Id::FatalPopup) {
let popup = Popup( let popup = Popup(
Size::Percentage(50), Size::Percentage(50),
self.calc_popup_height(Id::FatalPopup, f.size().width, f.size().height), self.calc_popup_height(Id::FatalPopup, f.area().width, f.area().height),
) )
.draw_in(f.size()); .draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::FatalPopup, f, popup); self.app.view(&Id::FatalPopup, f, popup);
} else if self.app.mounted(&Id::CopyPopup) { } else if self.app.mounted(&Id::CopyPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::CopyPopup, f, popup); self.app.view(&Id::CopyPopup, f, popup);
} else if self.app.mounted(&Id::ChmodPopup) { } else if self.app.mounted(&Id::ChmodPopup) {
let popup = Popup(Size::Percentage(50), Size::Unit(12)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Unit(12)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::ChmodPopup, f, popup); self.app.view(&Id::ChmodPopup, f, popup);
} else if self.app.mounted(&Id::FilterPopup) { } else if self.app.mounted(&Id::FilterPopup) {
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::FilterPopup, f, popup); self.app.view(&Id::FilterPopup, f, popup);
} else if self.app.mounted(&Id::GotoPopup) { } else if self.app.mounted(&Id::GotoPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::GotoPopup, f, popup); self.app.view(&Id::GotoPopup, f, popup);
} else if self.app.mounted(&Id::MkdirPopup) { } else if self.app.mounted(&Id::MkdirPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::MkdirPopup, f, popup); self.app.view(&Id::MkdirPopup, f, popup);
} else if self.app.mounted(&Id::NewfilePopup) { } else if self.app.mounted(&Id::NewfilePopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::NewfilePopup, f, popup); self.app.view(&Id::NewfilePopup, f, popup);
} else if self.app.mounted(&Id::OpenWithPopup) { } else if self.app.mounted(&Id::OpenWithPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::OpenWithPopup, f, popup); self.app.view(&Id::OpenWithPopup, f, popup);
} else if self.app.mounted(&Id::RenamePopup) { } else if self.app.mounted(&Id::RenamePopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::RenamePopup, f, popup); self.app.view(&Id::RenamePopup, f, popup);
} else if self.app.mounted(&Id::SaveAsPopup) { } else if self.app.mounted(&Id::SaveAsPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::SaveAsPopup, f, popup); self.app.view(&Id::SaveAsPopup, f, popup);
} else if self.app.mounted(&Id::SymlinkPopup) { } else if self.app.mounted(&Id::SymlinkPopup) {
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::SymlinkPopup, f, popup); self.app.view(&Id::SymlinkPopup, f, popup);
} else if self.app.mounted(&Id::ExecPopup) { } else if self.app.mounted(&Id::ExecPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::ExecPopup, f, popup); self.app.view(&Id::ExecPopup, f, popup);
} else if self.app.mounted(&Id::FileInfoPopup) { } else if self.app.mounted(&Id::FileInfoPopup) {
let popup = Popup(Size::Percentage(50), Size::Percentage(50)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Percentage(50)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::FileInfoPopup, f, popup); self.app.view(&Id::FileInfoPopup, f, popup);
} else if self.app.mounted(&Id::ProgressBarPartial) { } else if self.app.mounted(&Id::ProgressBarPartial) {
let popup = Popup(Size::Percentage(50), Size::Percentage(20)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Percentage(20)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
let popup_chunks = Layout::default() let popup_chunks = Layout::default()
@@ -241,14 +242,14 @@ impl FileTransferActivity {
self.app.view(&Id::ProgressBarFull, f, popup_chunks[0]); self.app.view(&Id::ProgressBarFull, f, popup_chunks[0]);
self.app.view(&Id::ProgressBarPartial, f, popup_chunks[1]); self.app.view(&Id::ProgressBarPartial, f, popup_chunks[1]);
} else if self.app.mounted(&Id::DeletePopup) { } else if self.app.mounted(&Id::DeletePopup) {
let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::DeletePopup, f, popup); self.app.view(&Id::DeletePopup, f, popup);
} else if self.app.mounted(&Id::ReplacePopup) { } else if self.app.mounted(&Id::ReplacePopup) {
// NOTE: handle extended / normal modes // NOTE: handle extended / normal modes
if self.is_radio_replace_extended() { if self.is_radio_replace_extended() {
let popup = Popup(Size::Percentage(50), Size::Percentage(50)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Percentage(50)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
let popup_chunks = Layout::default() let popup_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
@@ -264,42 +265,42 @@ impl FileTransferActivity {
.view(&Id::ReplacingFilesListPopup, f, popup_chunks[0]); .view(&Id::ReplacingFilesListPopup, f, popup_chunks[0]);
self.app.view(&Id::ReplacePopup, f, popup_chunks[1]); self.app.view(&Id::ReplacePopup, f, popup_chunks[1]);
} else { } else {
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::ReplacePopup, f, popup); self.app.view(&Id::ReplacePopup, f, popup);
} }
} else if self.app.mounted(&Id::DisconnectPopup) { } else if self.app.mounted(&Id::DisconnectPopup) {
let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::DisconnectPopup, f, popup); self.app.view(&Id::DisconnectPopup, f, popup);
} else if self.app.mounted(&Id::QuitPopup) { } else if self.app.mounted(&Id::QuitPopup) {
let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::QuitPopup, f, popup); self.app.view(&Id::QuitPopup, f, popup);
} else if self.app.mounted(&Id::WatchedPathsList) { } else if self.app.mounted(&Id::WatchedPathsList) {
let popup = Popup(Size::Percentage(60), Size::Percentage(50)).draw_in(f.size()); let popup = Popup(Size::Percentage(60), Size::Percentage(50)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::WatchedPathsList, f, popup); self.app.view(&Id::WatchedPathsList, f, popup);
} else if self.app.mounted(&Id::WatcherPopup) { } else if self.app.mounted(&Id::WatcherPopup) {
let popup = Popup(Size::Percentage(60), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(60), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::WatcherPopup, f, popup); self.app.view(&Id::WatcherPopup, f, popup);
} else if self.app.mounted(&Id::SortingPopup) { } else if self.app.mounted(&Id::SortingPopup) {
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::SortingPopup, f, popup); self.app.view(&Id::SortingPopup, f, popup);
} else if self.app.mounted(&Id::ErrorPopup) { } else if self.app.mounted(&Id::ErrorPopup) {
let popup = Popup( let popup = Popup(
Size::Percentage(50), Size::Percentage(50),
self.calc_popup_height(Id::ErrorPopup, f.size().width, f.size().height), self.calc_popup_height(Id::ErrorPopup, f.area().width, f.area().height),
) )
.draw_in(f.size()); .draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::ErrorPopup, f, popup); self.app.view(&Id::ErrorPopup, f, popup);
@@ -312,17 +313,17 @@ impl FileTransferActivity {
.unwrap_or(1) as u16; .unwrap_or(1) as u16;
let popup = let popup =
Popup(Size::Percentage(50), Size::Unit(2 + wait_popup_lines)).draw_in(f.size()); Popup(Size::Percentage(50), Size::Unit(2 + wait_popup_lines)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::WaitPopup, f, popup); self.app.view(&Id::WaitPopup, f, popup);
} else if self.app.mounted(&Id::SyncBrowsingMkdirPopup) { } else if self.app.mounted(&Id::SyncBrowsingMkdirPopup) {
let popup = Popup(Size::Percentage(60), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(60), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::SyncBrowsingMkdirPopup, f, popup); self.app.view(&Id::SyncBrowsingMkdirPopup, f, popup);
} else if self.app.mounted(&Id::KeybindingsPopup) { } else if self.app.mounted(&Id::KeybindingsPopup) {
let popup = Popup(Size::Percentage(50), Size::Percentage(80)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Percentage(80)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::KeybindingsPopup, f, popup); self.app.view(&Id::KeybindingsPopup, f, popup);
@@ -555,7 +556,7 @@ impl FileTransferActivity {
pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) { pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) {
// Get color // Get color
let (bg, fg, hg) = match self.browser.tab() { let (bg, fg, hg) = match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => ( FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => (
self.theme().transfer_local_explorer_background, self.theme().transfer_local_explorer_background,
self.theme().transfer_local_explorer_foreground, self.theme().transfer_local_explorer_foreground,
self.theme().transfer_local_explorer_highlighted, self.theme().transfer_local_explorer_highlighted,
@@ -763,7 +764,7 @@ impl FileTransferActivity {
pub(super) fn mount_file_sorting(&mut self) { pub(super) fn mount_file_sorting(&mut self) {
let sorting_color = self.theme().transfer_status_sorting; let sorting_color = self.theme().transfer_status_sorting;
let sorting: FileSorting = match self.browser.tab() { let sorting: FileSorting = match self.browser.tab() {
FileExplorerTab::Local => self.local().get_file_sorting(), FileExplorerTab::HostBridge => self.host_bridge().get_file_sorting(),
FileExplorerTab::Remote => self.remote().get_file_sorting(), FileExplorerTab::Remote => self.remote().get_file_sorting(),
_ => return, _ => return,
}; };
@@ -901,7 +902,7 @@ impl FileTransferActivity {
assert!(self assert!(self
.app .app
.remount( .remount(
Id::StatusBarLocal, Id::StatusBarHostBridge,
Box::new(components::StatusBarLocal::new( Box::new(components::StatusBarLocal::new(
&self.browser, &self.browser,
sorting_color, sorting_color,
@@ -1066,138 +1067,34 @@ impl FileTransferActivity {
/// Returns a sub clause which requires that no popup is mounted in order to be satisfied /// Returns a sub clause which requires that no popup is mounted in order to be satisfied
fn no_popup_mounted_clause() -> SubClause<Id> { fn no_popup_mounted_clause() -> SubClause<Id> {
SubClause::And( tuirealm::subclause_and_not!(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Id::CopyPopup,
Id::CopyPopup, Id::DeletePopup,
)))), Id::DisconnectPopup,
Box::new(SubClause::And( Id::ErrorPopup,
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Id::ExecPopup,
Id::DeletePopup, Id::FatalPopup,
)))), Id::FileInfoPopup,
Box::new(SubClause::And( Id::GotoPopup,
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Id::KeybindingsPopup,
Id::DisconnectPopup, Id::MkdirPopup,
)))), Id::NewfilePopup,
Box::new(SubClause::And( Id::OpenWithPopup,
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Id::ProgressBarFull,
Id::ErrorPopup, Id::ProgressBarPartial,
)))), Id::ExplorerFind,
Box::new(SubClause::And( Id::QuitPopup,
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Id::RenamePopup,
Id::ExecPopup, Id::ReplacePopup,
)))), Id::SaveAsPopup,
Box::new(SubClause::And( Id::SortingPopup,
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Id::SyncBrowsingMkdirPopup,
Id::FatalPopup, Id::SymlinkPopup,
)))), Id::WatcherPopup,
Box::new(SubClause::And( Id::WatchedPathsList,
Box::new(SubClause::Not(Box::new(SubClause::IsMounted( Id::ChmodPopup,
Id::FileInfoPopup, Id::WaitPopup,
)))), Id::FilterPopup
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::GotoPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::KeybindingsPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::MkdirPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::NewfilePopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::OpenWithPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::ProgressBarFull,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::ProgressBarPartial,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::ExplorerFind,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::QuitPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::RenamePopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::ReplacePopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SaveAsPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SortingPopup,
)))),
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::WatcherPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WatchedPathsList,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::ChmodPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WaitPopup,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::FilterPopup,
)))),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
)),
) )
} }
} }

View File

@@ -10,6 +10,8 @@ pub mod auth;
pub mod filetransfer; pub mod filetransfer;
pub mod setup; pub mod setup;
const CROSSTERM_MAX_POLL: usize = 10;
// -- Exit reason // -- Exit reason
pub enum ExitReason { pub enum ExitReason {

View File

@@ -6,7 +6,7 @@
// Locals // Locals
use std::env; use std::env;
use tuirealm::tui::style::Color; use tuirealm::ratatui::style::Color;
use tuirealm::{State, StateValue}; use tuirealm::{State, StateValue};
use super::{Id, IdSsh, IdTheme, SetupActivity, ViewLayout}; use super::{Id, IdSsh, IdTheme, SetupActivity, ViewLayout};

View File

@@ -19,7 +19,7 @@ use tuirealm::listener::EventListenerCfg;
use tuirealm::props::Color; use tuirealm::props::Color;
use tuirealm::{Application, NoUserEvent, Update}; use tuirealm::{Application, NoUserEvent, Update};
use super::{Activity, Context, ExitReason}; use super::{Activity, Context, ExitReason, CROSSTERM_MAX_POLL};
use crate::config::themes::Theme; use crate::config::themes::Theme;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
use crate::system::theme_provider::ThemeProvider; use crate::system::theme_provider::ThemeProvider;
@@ -262,7 +262,7 @@ impl SetupActivity {
Self { Self {
app: Application::init( app: Application::init(
EventListenerCfg::default() EventListenerCfg::default()
.default_input_listener(ticks) .crossterm_input_listener(ticks, CROSSTERM_MAX_POLL)
.poll_timeout(ticks), .poll_timeout(ticks),
), ),
exit_reason: None, exit_reason: None,

View File

@@ -8,7 +8,7 @@ pub mod ssh_keys;
pub mod theme; pub mod theme;
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::tui::widgets::Clear; use tuirealm::ratatui::widgets::Clear;
use tuirealm::{Frame, Sub, SubClause, SubEventClause}; use tuirealm::{Frame, Sub, SubClause, SubEventClause};
use super::*; use super::*;
@@ -112,23 +112,23 @@ impl SetupActivity {
pub(super) fn view_popups(&mut self, f: &mut Frame) { pub(super) fn view_popups(&mut self, f: &mut Frame) {
if self.app.mounted(&Id::Common(IdCommon::ErrorPopup)) { if self.app.mounted(&Id::Common(IdCommon::ErrorPopup)) {
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
// make popup // make popup
self.app.view(&Id::Common(IdCommon::ErrorPopup), f, popup); self.app.view(&Id::Common(IdCommon::ErrorPopup), f, popup);
} else if self.app.mounted(&Id::Common(IdCommon::QuitPopup)) { } else if self.app.mounted(&Id::Common(IdCommon::QuitPopup)) {
// make popup // make popup
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
self.app.view(&Id::Common(IdCommon::QuitPopup), f, popup); self.app.view(&Id::Common(IdCommon::QuitPopup), f, popup);
} else if self.app.mounted(&Id::Common(IdCommon::Keybindings)) { } else if self.app.mounted(&Id::Common(IdCommon::Keybindings)) {
// make popup // make popup
let popup = Popup(Size::Percentage(50), Size::Percentage(70)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Percentage(70)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
self.app.view(&Id::Common(IdCommon::Keybindings), f, popup); self.app.view(&Id::Common(IdCommon::Keybindings), f, popup);
} else if self.app.mounted(&Id::Common(IdCommon::SavePopup)) { } else if self.app.mounted(&Id::Common(IdCommon::SavePopup)) {
// make popup // make popup
let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
self.app.view(&Id::Common(IdCommon::SavePopup), f, popup); self.app.view(&Id::Common(IdCommon::SavePopup), f, popup);
} }
@@ -235,33 +235,13 @@ impl SetupActivity {
/// Returns a sub clause which requires that no popup is mounted in order to be satisfied /// Returns a sub clause which requires that no popup is mounted in order to be satisfied
fn no_popup_mounted_clause() -> SubClause<Id> { fn no_popup_mounted_clause() -> SubClause<Id> {
SubClause::And( tuirealm::subclause_and_not!(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common( Id::Common(IdCommon::ErrorPopup),
IdCommon::ErrorPopup, Id::Common(IdCommon::Keybindings),
))))), Id::Common(IdCommon::QuitPopup),
Box::new(SubClause::And( Id::Common(IdCommon::SavePopup),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common( Id::Ssh(IdSsh::DelSshKeyPopup),
IdCommon::Keybindings, Id::Ssh(IdSsh::SshHost)
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::QuitPopup,
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
IdCommon::SavePopup,
))))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Ssh(
IdSsh::DelSshKeyPopup,
))))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Ssh(
IdSsh::SshHost,
))))),
)),
)),
)),
)),
) )
} }
} }

View File

@@ -7,7 +7,7 @@
// Ext // Ext
use std::path::PathBuf; use std::path::PathBuf;
use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::{State, StateValue}; use tuirealm::{State, StateValue};
use super::{ use super::{
@@ -50,7 +50,7 @@ impl SetupActivity {
] ]
.as_ref(), .as_ref(),
) )
.split(f.size()); .split(f.area());
// Render common widget // Render common widget
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]); self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]); self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);

View File

@@ -5,8 +5,8 @@
// Locals // Locals
// Ext // Ext
use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::tui::widgets::Clear; use tuirealm::ratatui::widgets::Clear;
use super::{components, Context, Id, IdCommon, IdSsh, SetupActivity, ViewLayout}; use super::{components, Context, Id, IdCommon, IdSsh, SetupActivity, ViewLayout};
use crate::utils::ui::{Popup, Size}; use crate::utils::ui::{Popup, Size};
@@ -37,7 +37,7 @@ impl SetupActivity {
] ]
.as_ref(), .as_ref(),
) )
.split(f.size()); .split(f.area());
// Render common widget // Render common widget
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]); self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]); self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
@@ -45,11 +45,11 @@ impl SetupActivity {
// Popups // Popups
self.view_popups(f); self.view_popups(f);
if self.app.mounted(&Id::Ssh(IdSsh::DelSshKeyPopup)) { if self.app.mounted(&Id::Ssh(IdSsh::DelSshKeyPopup)) {
let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.size()); let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
self.app.view(&Id::Ssh(IdSsh::DelSshKeyPopup), f, popup); self.app.view(&Id::Ssh(IdSsh::DelSshKeyPopup), f, popup);
} else if self.app.mounted(&Id::Ssh(IdSsh::SshHost)) { } else if self.app.mounted(&Id::Ssh(IdSsh::SshHost)) {
let popup = Popup(Size::Percentage(50), Size::Percentage(20)).draw_in(f.size()); let popup = Popup(Size::Percentage(50), Size::Percentage(20)).draw_in(f.area());
f.render_widget(Clear, popup); f.render_widget(Clear, popup);
let popup_chunks = Layout::default() let popup_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)

View File

@@ -5,7 +5,7 @@
// Locals // Locals
// Ext // Ext
use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use super::{components, Context, Id, IdCommon, IdTheme, SetupActivity, Theme, ViewLayout}; use super::{components, Context, Id, IdCommon, IdTheme, SetupActivity, Theme, ViewLayout};
@@ -39,7 +39,7 @@ impl SetupActivity {
] ]
.as_ref(), .as_ref(),
) )
.split(f.size()); .split(f.area());
// Render common widget // Render common widget
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]); self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]); self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);

View File

@@ -3,21 +3,22 @@
//! `Context` is the module which provides all the functionalities related to the UI data holder, called Context //! `Context` is the module which provides all the functionalities related to the UI data holder, called Context
// Locals // Locals
use tuirealm::terminal::TerminalBridge; use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalBridge};
use super::store::Store; use super::store::Store;
use crate::filetransfer::FileTransferParams; use crate::filetransfer::{FileTransferParams, HostBridgeParams};
use crate::system::bookmarks_client::BookmarksClient; use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
use crate::system::theme_provider::ThemeProvider; use crate::system::theme_provider::ThemeProvider;
/// Context holds data structures shared by the activities /// Context holds data structures shared by the activities
pub struct Context { pub struct Context {
ft_params: Option<FileTransferParams>, host_bridge_params: Option<HostBridgeParams>,
remote_params: Option<FileTransferParams>,
bookmarks_client: Option<BookmarksClient>, bookmarks_client: Option<BookmarksClient>,
config_client: ConfigClient, config_client: ConfigClient,
pub(crate) store: Store, pub(crate) store: Store,
pub(crate) terminal: TerminalBridge, pub(crate) terminal: TerminalBridge<CrosstermTerminalAdapter>,
theme_provider: ThemeProvider, theme_provider: ThemeProvider,
error: Option<String>, error: Option<String>,
} }
@@ -30,27 +31,29 @@ impl Context {
theme_provider: ThemeProvider, theme_provider: ThemeProvider,
error: Option<String>, error: Option<String>,
) -> Context { ) -> Context {
let mut ctx = Context { let mut terminal = TerminalBridge::init_crossterm().expect("Could not initialize terminal");
let _ = terminal.disable_mouse_capture();
Context {
bookmarks_client, bookmarks_client,
config_client, config_client,
ft_params: None, host_bridge_params: None,
remote_params: None,
store: Store::init(), store: Store::init(),
terminal: TerminalBridge::new().expect("Could not initialize terminal"), terminal,
theme_provider, theme_provider,
error, error,
}; }
// Init terminal state
let _ = ctx.terminal.enable_raw_mode();
let _ = ctx.terminal.enter_alternate_screen();
ctx
} }
// -- getters // -- getters
pub fn ft_params(&self) -> Option<&FileTransferParams> { pub fn remote_params(&self) -> Option<&FileTransferParams> {
self.ft_params.as_ref() self.remote_params.as_ref()
}
pub fn host_bridge_params(&self) -> Option<&HostBridgeParams> {
self.host_bridge_params.as_ref()
} }
pub fn bookmarks_client(&self) -> Option<&BookmarksClient> { pub fn bookmarks_client(&self) -> Option<&BookmarksClient> {
@@ -85,23 +88,22 @@ impl Context {
&mut self.theme_provider &mut self.theme_provider
} }
pub fn terminal(&mut self) -> &mut TerminalBridge { pub fn terminal(&mut self) -> &mut TerminalBridge<CrosstermTerminalAdapter> {
&mut self.terminal &mut self.terminal
} }
// -- setter // -- setter
pub fn set_ftparams(&mut self, params: FileTransferParams) { pub fn set_remote_params(&mut self, params: FileTransferParams) {
self.ft_params = Some(params); self.remote_params = Some(params);
}
pub fn set_host_bridge_params(&mut self, params: HostBridgeParams) {
self.host_bridge_params = Some(params);
} }
// -- error // -- error
/// Set context error
pub fn set_error(&mut self, err: String) {
self.error = Some(err);
}
/// Get error message and remove it from the context /// Get error message and remove it from the context
pub fn error(&mut self) -> Option<String> { pub fn error(&mut self) -> Option<String> {
self.error.take() self.error.take()
@@ -110,8 +112,8 @@ impl Context {
impl Drop for Context { impl Drop for Context {
fn drop(&mut self) { fn drop(&mut self) {
// Re-enable terminal stuff if let Err(err) = self.terminal.restore() {
let _ = self.terminal.disable_raw_mode(); error!("Could not restore terminal: {err}");
let _ = self.terminal.leave_alternate_screen(); }
} }
} }

Some files were not shown because too many files have changed in this diff Show More