mirror of
https://github.com/veeso/termscp.git
synced 2025-12-06 17:15:35 -08:00
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,6 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
- [Changelog](#changelog)
|
||||
- [0.16.0](#0160)
|
||||
- [0.15.0](#0150)
|
||||
- [0.14.0](#0140)
|
||||
- [0.13.0](#0130)
|
||||
@@ -37,6 +38,24 @@
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
Released on 03/10/2024
|
||||
|
||||
740
Cargo.lock
generated
740
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -10,7 +10,7 @@ license = "MIT"
|
||||
name = "termscp"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
version = "0.15.0"
|
||||
version = "0.16.0"
|
||||
|
||||
[package.metadata.rpm]
|
||||
package = "termscp"
|
||||
@@ -77,8 +77,8 @@ tempfile = "^3"
|
||||
thiserror = "^1"
|
||||
tokio = { version = "=1.38.1", features = ["rt"] }
|
||||
toml = "^0.8"
|
||||
tui-realm-stdlib = "^1.3"
|
||||
tuirealm = "^1.9"
|
||||
tui-realm-stdlib = "2"
|
||||
tuirealm = "2"
|
||||
unicode-width = "^0.2"
|
||||
version-compare = "^0.2"
|
||||
whoami = "^1.5"
|
||||
@@ -90,6 +90,8 @@ serial_test = "^3"
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2"
|
||||
vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] }
|
||||
|
||||
|
||||
[features]
|
||||
default = ["smb", "with-keyring"]
|
||||
@@ -110,7 +112,7 @@ remotefs-ssh = "^0.4"
|
||||
[target."cfg(target_family = \"unix\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
|
||||
remotefs-ssh = { version = "^0.4", features = ["ssh2-vendored"] }
|
||||
users = "0.11.0"
|
||||
uzers = "0.12"
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Developed by <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Current version: 0.15.0 (03/10/2024)</p>
|
||||
<p align="center">Current version: 0.16.0 (14/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
19
build.rs
19
build.rs
@@ -1,6 +1,7 @@
|
||||
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
|
||||
cfg_aliases! {
|
||||
// Platforms
|
||||
@@ -13,4 +14,20 @@ fn main() {
|
||||
smb_unix: { all(unix, feature = "smb", not(macos)) },
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Entwickelt von <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Aktuelle Version: 0.15.0 (03/10/2024)</p>
|
||||
<p align="center">Aktuelle Version: 0.16.0 (14/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -62,11 +62,11 @@
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
- `-b, --address-as-bookmark` löst das Adressargument als Lesezeichenname auf
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Desarrollado por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versión actual: 0.15.0 (03/10/2024)</p>
|
||||
<p align="center">Versión actual: 0.16.0 (14/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -39,11 +39,11 @@
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
- `-b, --address-as-bookmark` resuelve el argumento de la dirección como un nombre de marcador
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Développé par <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Version actuelle: 0.15.0 (03/10/2024)</p>
|
||||
<p align="center">Version actuelle: 0.16.0 (14/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
- `-b, --address-as-bookmark` résoudre l'argument d'adresse en tant que nom de signet
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Sviluppato da <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versione corrente: 0.15.0 (03/10/2024)</p>
|
||||
<p align="center">Versione corrente: 0.16.0 (14/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
- `-b, --address-as-bookmark` risolve l'argomento indirizzo come nome di un segnalibro
|
||||
|
||||
@@ -40,13 +40,15 @@
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
- `-q, --quiet` Disable logging
|
||||
- `-v, --version` Print version info
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">Desenvolvido por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versão atual: 0.15.0 (03/10/2024)</p>
|
||||
<p align="center">Versão atual: 0.16.0 (14/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -40,11 +40,11 @@
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
- `-b, --address-as-bookmark` resolve o argumento do endereço como um nome de favorito
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</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.0 (14/10/2024)</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
|
||||
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>` 登陆密码
|
||||
- `-b, --address-as-bookmark` 将地址参数解析为书签名称
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# -f, -y, --force, --yes
|
||||
# Skip the confirmation prompt during installation
|
||||
|
||||
TERMSCP_VERSION="0.15.0"
|
||||
TERMSCP_VERSION="0.16.0"
|
||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
||||
since last release, so if the latest version is not available yet,
|
||||
you can install it downloading the ZIP file from</span>
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.15.0.nupkg"
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.16.0.nupkg"
|
||||
target="_blank">Github</a>
|
||||
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
||||
</p>
|
||||
@@ -74,7 +74,7 @@
|
||||
On Debian based distros, you can install termscp using the Deb
|
||||
package via:
|
||||
</p>
|
||||
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.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.0_amd64.deb</span>
|
||||
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
|
||||
</div>
|
||||
<h3>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</button>
|
||||
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
||||
<p class="text-lg">
|
||||
<span translate="intro.versionAlert">termscp 0.15.0 is NOW out! Download it from</span>
|
||||
<span translate="intro.versionAlert">termscp 0.16.0 is NOW out! Download it from</span>
|
||||
<a href="/get-started.html" translate="intro.here">here!</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Get started →",
|
||||
"versionAlert": "termscp 0.15.0 is NOW out! Download it from",
|
||||
"versionAlert": "termscp 0.16.0 is NOW out! Download it from",
|
||||
"here": "here",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Para iniciar →",
|
||||
"versionAlert": "termscp 0.15.0 ya está disponible! Descárgalo desde",
|
||||
"versionAlert": "termscp 0.16.0 ya está disponible! Descárgalo desde",
|
||||
"here": "aquì",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Pour commencer →",
|
||||
"versionAlert": "termscp 0.15.0 est maintenant sorti! Télécharge-le depuis",
|
||||
"versionAlert": "termscp 0.16.0 est maintenant sorti! Télécharge-le depuis",
|
||||
"here": "ici",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
||||
"getStarted": "Installa termscp →",
|
||||
"versionAlert": "termscp 0.15.0 è ORA disponbile! Scaricalo da",
|
||||
"versionAlert": "termscp 0.16.0 è ORA disponbile! Scaricalo da",
|
||||
"here": "qui",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "开始 →",
|
||||
"versionAlert": "termscp 0.15.0 现已发布! 从下载",
|
||||
"versionAlert": "termscp 0.16.0 现已发布! 从下载",
|
||||
"here": "这里",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
//!
|
||||
//! `activity_manager` is the module which provides run methods and handling for activities
|
||||
|
||||
// Deps
|
||||
// Namespaces
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
|
||||
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::host::{HostError, Localhost};
|
||||
use crate::cli::{Remote, RemoteArgs};
|
||||
use crate::filetransfer::{
|
||||
FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
|
||||
};
|
||||
use crate::host::HostError;
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::environment;
|
||||
@@ -30,6 +32,16 @@ pub enum NextActivity {
|
||||
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
|
||||
pub struct ActivityManager {
|
||||
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,
|
||||
¶ms.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, ¶ms.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
|
||||
pub fn set_filetransfer_params(
|
||||
pub fn set_host_params(
|
||||
&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>,
|
||||
) -> Result<(), String> {
|
||||
// Set password if provided
|
||||
@@ -73,13 +175,13 @@ impl ActivityManager {
|
||||
if let Some(password) = password {
|
||||
params.set_default_secret(password.to_string());
|
||||
} else if matches!(
|
||||
params.protocol,
|
||||
protocol,
|
||||
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
|
||||
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
|
||||
.resolve(
|
||||
&generic_params.address,
|
||||
@@ -94,7 +196,7 @@ impl ActivityManager {
|
||||
"storage could not find any suitable key for {}... prompting for password",
|
||||
generic_params.address
|
||||
);
|
||||
self.prompt_password(&mut params)?;
|
||||
self.prompt_password(params)?;
|
||||
} else {
|
||||
debug!(
|
||||
"a key is already set for {}; password is not required",
|
||||
@@ -102,17 +204,19 @@ impl ActivityManager {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.prompt_password(&mut params)?;
|
||||
self.prompt_password(params)?;
|
||||
}
|
||||
}
|
||||
// Put params into the context
|
||||
self.context.as_mut().unwrap().set_ftparams(params);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prompt user for password to set into params.
|
||||
fn prompt_password(&self, params: &mut FileTransferParams) -> Result<(), String> {
|
||||
match tty::read_secret_from_tty("Password: ") {
|
||||
fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> {
|
||||
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}")),
|
||||
Ok(Some(secret)) => {
|
||||
debug!(
|
||||
@@ -130,16 +234,28 @@ impl ActivityManager {
|
||||
/// Returns error if bookmark is not found
|
||||
pub fn resolve_bookmark_name(
|
||||
&mut self,
|
||||
host: Host,
|
||||
bookmark_name: &str,
|
||||
password: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() {
|
||||
match bookmarks_client.get_bookmark(bookmark_name) {
|
||||
None => Err(format!(
|
||||
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
|
||||
)),
|
||||
Some(params) => self.set_filetransfer_params(params, password),
|
||||
}
|
||||
let params = match bookmarks_client.get_bookmark(bookmark_name) {
|
||||
None => {
|
||||
return Err(format!(
|
||||
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
|
||||
))
|
||||
}
|
||||
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 {
|
||||
Err(String::from(
|
||||
"Could not resolve bookmark name: bookmarks client not initialized",
|
||||
@@ -226,15 +342,24 @@ impl ActivityManager {
|
||||
fn run_filetransfer(&mut self) -> Option<NextActivity> {
|
||||
info!("Starting FileTransferActivity");
|
||||
// Get context
|
||||
let mut ctx: Context = match self.context.take() {
|
||||
let ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
None => {
|
||||
error!("Failed to start FileTransferActivity: context is 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
|
||||
let ft_params: &FileTransferParams = match ctx.ft_params() {
|
||||
let remote_params: &FileTransferParams = match ctx.remote_params() {
|
||||
Some(ft_params) => ft_params,
|
||||
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 =
|
||||
FileTransferActivity::new(host, ft_params, self.ticks);
|
||||
FileTransferActivity::new(host_bridge_params, remote_params, self.ticks);
|
||||
// Prepare result
|
||||
let result: Option<NextActivity>;
|
||||
// Create activity
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
//!
|
||||
//! defines the types for main.rs types
|
||||
|
||||
mod remote;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use argh::FromArgs;
|
||||
pub use remote::{Remote, RemoteArgs};
|
||||
|
||||
use crate::activity_manager::NextActivity;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::system::logging::LogLevel;
|
||||
|
||||
pub enum Task {
|
||||
@@ -17,12 +19,14 @@ pub enum Task {
|
||||
InstallUpdate,
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
#[derive(Default, FromArgs)]
|
||||
#[argh(description = "
|
||||
where positional can be:
|
||||
- [address] [local-wrkdir]
|
||||
- [address_a] [address_b] [local-wrkdir]
|
||||
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:
|
||||
|
||||
@@ -37,14 +41,15 @@ pub struct Args {
|
||||
#[argh(subcommand)]
|
||||
pub nested: Option<ArgsSubcommands>,
|
||||
/// resolve address argument as a bookmark name
|
||||
#[argh(switch, short = 'b')]
|
||||
pub address_as_bookmark: bool,
|
||||
#[argh(option, short = 'b')]
|
||||
pub bookmark: Vec<String>,
|
||||
/// enable TRACE log level
|
||||
#[argh(switch, short = 'D')]
|
||||
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')]
|
||||
pub password: Option<String>,
|
||||
pub password: Vec<String>,
|
||||
/// disable logging
|
||||
#[argh(switch, short = 'q')]
|
||||
pub quiet: bool,
|
||||
@@ -55,10 +60,7 @@ pub struct Args {
|
||||
#[argh(switch, short = 'v')]
|
||||
pub version: bool,
|
||||
// -- positional
|
||||
#[argh(
|
||||
positional,
|
||||
description = "protocol://user@address:port:wrkdir local-wrkdir"
|
||||
)]
|
||||
#[argh(positional, description = "address1 address2 local-wrkdir")]
|
||||
pub positional: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -90,7 +92,7 @@ pub struct LoadThemeArgs {
|
||||
}
|
||||
|
||||
pub struct RunOpts {
|
||||
pub remote: Remote,
|
||||
pub remote: RemoteArgs,
|
||||
pub ticks: Duration,
|
||||
pub log_level: LogLevel,
|
||||
pub task: Task,
|
||||
@@ -122,45 +124,10 @@ impl RunOpts {
|
||||
impl Default for RunOpts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
remote: Remote::None,
|
||||
remote: RemoteArgs::default(),
|
||||
ticks: Duration::from_millis(10),
|
||||
log_level: LogLevel::Info,
|
||||
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
271
src/cli/remote.rs
Normal 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(unix)]
|
||||
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(unix)]
|
||||
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(unix)]
|
||||
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")));
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
|
||||
use super::*;
|
||||
use crate::config::bookmarks::{Bookmark, KubeParams, S3Params, SmbParams, UserHosts};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// ext
|
||||
use serde::de::Error as DeError;
|
||||
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::parser::parse_color;
|
||||
|
||||
@@ -12,7 +12,7 @@ use lazy_regex::{Lazy, Regex};
|
||||
use remotefs::File;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
#[cfg(unix)]
|
||||
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::path::diff_paths;
|
||||
|
||||
21
src/filetransfer/host_bridge_builder.rs
Normal file
21
src/filetransfer/host_bridge_builder.rs
Normal 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),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
//!
|
||||
//! `filetransfer` is the module which provides the file transfer protocols and remotefs builders
|
||||
|
||||
mod builder;
|
||||
mod host_bridge_builder;
|
||||
pub mod params;
|
||||
mod remotefs_builder;
|
||||
|
||||
// -- export types
|
||||
pub use builder::Builder;
|
||||
pub use params::{FileTransferParams, ProtocolParams};
|
||||
pub use host_bridge_builder::HostBridgeBuilder;
|
||||
pub use params::{FileTransferParams, HostBridgeParams, ProtocolParams};
|
||||
pub use remotefs_builder::RemoteFsBuilder;
|
||||
|
||||
/// This enum defines the different transfer protocol available in termscp
|
||||
|
||||
|
||||
@@ -15,6 +15,24 @@ pub use self::smb::SmbParams;
|
||||
pub use self::webdav::WebDAVProtocolParams;
|
||||
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
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileTransferParams {
|
||||
@@ -34,6 +52,43 @@ pub enum ProtocolParams {
|
||||
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
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GenericProtocolParams {
|
||||
@@ -68,25 +123,15 @@ impl FileTransferParams {
|
||||
|
||||
/// 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!!!
|
||||
#[cfg(test)]
|
||||
pub fn password_missing(&self) -> bool {
|
||||
match &self.params {
|
||||
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(),
|
||||
}
|
||||
self.params.password_missing()
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
match &mut self.params {
|
||||
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),
|
||||
}
|
||||
self.params.set_default_secret(secret);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::ssh as ssh_utils;
|
||||
|
||||
/// Remotefs builder
|
||||
pub struct Builder;
|
||||
pub struct RemoteFsBuilder;
|
||||
|
||||
impl Builder {
|
||||
impl RemoteFsBuilder {
|
||||
/// Build RemoteFs client from protocol and params.
|
||||
///
|
||||
/// if protocol and parameters are inconsistent, the function will panic.
|
||||
@@ -262,7 +262,7 @@ mod test {
|
||||
.session_token(Some("gerry-scotti")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -275,7 +275,7 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
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]
|
||||
@@ -288,7 +288,7 @@ mod test {
|
||||
client_key: Some("client_key".to_string()),
|
||||
});
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Kube, params, &config_client);
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -301,7 +301,7 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Scp, params, &config_client);
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::Scp, params, &config_client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -314,7 +314,7 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client);
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::Sftp, params, &config_client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -322,7 +322,7 @@ mod test {
|
||||
fn should_build_smb_fs() {
|
||||
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Smb, params, &config_client);
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -336,7 +336,7 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
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 {
|
||||
84
src/host/bridge.rs
Normal file
84
src/host/bridge.rs
Normal 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
1042
src/host/localhost.rs
Normal file
File diff suppressed because it is too large
Load Diff
986
src/host/mod.rs
986
src/host/mod.rs
File diff suppressed because it is too large
Load Diff
211
src/host/remote_bridged.rs
Normal file
211
src/host/remote_bridged.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
120
src/host/remote_bridged/temp_mapped_file.rs
Normal file
120
src/host/remote_bridged/temp_mapped_file.rs
Normal 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!");
|
||||
}
|
||||
}
|
||||
129
src/main.rs
129
src/main.rs
@@ -1,5 +1,13 @@
|
||||
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
mod activity_manager;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod explorer;
|
||||
mod filetransfer;
|
||||
mod host;
|
||||
mod support;
|
||||
mod system;
|
||||
mod ui;
|
||||
mod utils;
|
||||
|
||||
// Crates
|
||||
#[macro_use]
|
||||
@@ -13,28 +21,27 @@ extern crate log;
|
||||
#[macro_use]
|
||||
extern crate magic_crypt;
|
||||
|
||||
// External libs
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
// Include
|
||||
mod activity_manager;
|
||||
mod cli_opts;
|
||||
mod config;
|
||||
mod explorer;
|
||||
mod filetransfer;
|
||||
mod host;
|
||||
mod support;
|
||||
mod system;
|
||||
mod ui;
|
||||
mod utils;
|
||||
use self::activity_manager::{ActivityManager, NextActivity};
|
||||
use self::cli::{Args, ArgsSubcommands, RemoteArgs, RunOpts, Task};
|
||||
use self::system::logging::{self, LogLevel};
|
||||
|
||||
// namespaces
|
||||
use activity_manager::{ActivityManager, NextActivity};
|
||||
use cli_opts::{Args, ArgsSubcommands, BookmarkParams, HostParams, Remote, RunOpts, Task};
|
||||
use filetransfer::FileTransferParams;
|
||||
use system::logging::{self, LogLevel};
|
||||
const APP_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
const APP_BUILD_DATE: &str = env!("VERGEN_BUILD_TIMESTAMP");
|
||||
const APP_GIT_BRANCH: &str = env!("VERGEN_GIT_BRANCH");
|
||||
const APP_GIT_HASH: &str = env!("VERGEN_GIT_SHA");
|
||||
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() {
|
||||
let args: Args = argh::from_env();
|
||||
@@ -50,7 +57,10 @@ fn main() {
|
||||
if let Err(err) = logging::init(run_opts.log_level) {
|
||||
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
|
||||
info!("Starting activity manager...");
|
||||
let rc = run(run_opts);
|
||||
@@ -72,7 +82,8 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
// Version
|
||||
if args.version {
|
||||
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
|
||||
@@ -84,22 +95,24 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
// Match ticks
|
||||
run_opts.ticks = Duration::from_millis(args.ticks);
|
||||
// Remote argument
|
||||
match parse_address_arg(&args) {
|
||||
match RemoteArgs::try_from(&args) {
|
||||
Err(err) => return Err(err),
|
||||
Ok(Remote::None) => {}
|
||||
Ok(remote) => {
|
||||
// Set params
|
||||
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
|
||||
if let Some(localdir) = args.positional.get(1) {
|
||||
// Change working directory if local dir is set
|
||||
let localdir: PathBuf = PathBuf::from(localdir);
|
||||
if let Err(err) = env::set_current_dir(localdir.as_path()) {
|
||||
if let Some(localdir) = run_opts.remote.local_dir.as_deref() {
|
||||
if let Err(err) = env::set_current_dir(localdir) {
|
||||
return Err(format!("Bad working directory argument: {err}"));
|
||||
}
|
||||
}
|
||||
@@ -111,29 +124,6 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
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
|
||||
fn run(run_opts: RunOpts) -> i32 {
|
||||
match run_opts.task {
|
||||
@@ -147,11 +137,11 @@ fn run_import_theme(theme: &Path) -> i32 {
|
||||
match support::import_theme(theme) {
|
||||
Ok(_) => {
|
||||
println!("Theme has been successfully imported!");
|
||||
0
|
||||
EXIT_CODE_ERROR
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
1
|
||||
EXIT_CODE_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,41 +150,32 @@ fn run_install_update() -> i32 {
|
||||
match support::install_update() {
|
||||
Ok(msg) => {
|
||||
println!("{msg}");
|
||||
0
|
||||
EXIT_CODE_SUCCESS
|
||||
}
|
||||
Err(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)
|
||||
let mut manager: ActivityManager = match ActivityManager::new(ticks) {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
eprintln!("Could not start activity manager: {err}");
|
||||
return 1;
|
||||
return EXIT_CODE_ERROR;
|
||||
}
|
||||
};
|
||||
|
||||
// Set file transfer params if set
|
||||
match remote {
|
||||
Remote::Bookmark(BookmarkParams { name, password }) => {
|
||||
if let Err(err) = manager.resolve_bookmark_name(&name, password.as_deref()) {
|
||||
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 => {}
|
||||
if let Err(err) = manager.configure_remote_args(remote_args) {
|
||||
eprintln!("{err}");
|
||||
return EXIT_CODE_ERROR;
|
||||
}
|
||||
|
||||
manager.run(activity);
|
||||
|
||||
0
|
||||
EXIT_CODE_SUCCESS
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ impl Update {
|
||||
|
||||
/// In case received version is newer than current one, version as Some is returned; otherwise None
|
||||
fn check_version(r: Release) -> Option<Release> {
|
||||
debug!("got version from GitHub: {}", r.version);
|
||||
match parse_semver(r.version.as_str()) {
|
||||
Some(new_version) => {
|
||||
// Check if version is different
|
||||
|
||||
@@ -25,7 +25,11 @@ pub fn init(level: LogLevel) -> Result<(), String> {
|
||||
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))?;
|
||||
// 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
|
||||
WriteLogger::init(level, config, file).map_err(|e| format!("Failed to initialize logger: {e}"))
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ impl ThemeProvider {
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ impl FileToRemove {
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FileUpdate {
|
||||
/// Path to file which has changed
|
||||
local: PathBuf,
|
||||
host_bridge: PathBuf,
|
||||
/// Path to remote file to update
|
||||
remote: PathBuf,
|
||||
}
|
||||
@@ -152,13 +152,13 @@ impl FileUpdate {
|
||||
fn new(changed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self {
|
||||
Self {
|
||||
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
|
||||
pub fn local(&self) -> &Path {
|
||||
self.local.as_path()
|
||||
pub fn host_bridge(&self) -> &Path {
|
||||
self.host_bridge.as_path()
|
||||
}
|
||||
|
||||
/// Get path to remote file to sync
|
||||
@@ -288,7 +288,7 @@ mod test {
|
||||
Path::new("/home/foo/bar.txt"),
|
||||
);
|
||||
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"));
|
||||
} else {
|
||||
panic!("not an update");
|
||||
@@ -303,7 +303,7 @@ mod test {
|
||||
Path::new("/home/foo/temp"),
|
||||
);
|
||||
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"));
|
||||
} else {
|
||||
panic!("not an update");
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
// Locals
|
||||
use super::{AuthActivity, FileTransferParams};
|
||||
use super::{AuthActivity, FileTransferParams, FormTab, HostBridgeProtocol};
|
||||
use crate::filetransfer::params::{
|
||||
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
|
||||
WebDAVProtocolParams,
|
||||
};
|
||||
use crate::filetransfer::HostBridgeParams;
|
||||
|
||||
impl AuthActivity {
|
||||
/// Delete bookmark
|
||||
@@ -26,27 +27,49 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
// Iterate over bookmarks
|
||||
if let Some(key) = self.bookmarks_list.get(idx) {
|
||||
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
|
||||
// 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
|
||||
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) {
|
||||
let params = match self.collect_host_params() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
return;
|
||||
}
|
||||
pub(super) fn save_bookmark(&mut self, form_tab: FormTab, name: String, save_password: bool) {
|
||||
let params = match form_tab {
|
||||
FormTab::Remote => match self.collect_remote_host_params() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
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() {
|
||||
bookmarks_cli.add_bookmark(name.clone(), params, save_password);
|
||||
// Save bookmarks
|
||||
@@ -73,13 +96,16 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
// Iterate over bookmarks
|
||||
if let Some(key) = self.recents_list.get(idx) {
|
||||
if let Some(bookmark) = client.get_recent(key) {
|
||||
// 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"
|
||||
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,
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
@@ -147,73 +173,125 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// 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
|
||||
self.protocol = bookmark.protocol;
|
||||
self.mount_protocol(bookmark.protocol);
|
||||
self.host_bridge_protocol = HostBridgeProtocol::Remote(bookmark.protocol);
|
||||
self.mount_host_bridge_protocol(self.host_bridge_protocol);
|
||||
self.mount_remote_directory(
|
||||
FormTab::HostBridge,
|
||||
bookmark
|
||||
.remote_path
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
self.mount_local_directory(
|
||||
FormTab::HostBridge,
|
||||
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(params),
|
||||
ProtocolParams::Kube(params) => self.load_bookmark_kube_into_gui(params),
|
||||
ProtocolParams::AwsS3(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::Smb(params) => self.load_bookmark_smb_into_gui(params),
|
||||
ProtocolParams::WebDAV(params) => self.load_bookmark_webdav_into_gui(params),
|
||||
ProtocolParams::Generic(params) => {
|
||||
self.load_bookmark_generic_into_gui(FormTab::HostBridge, 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) {
|
||||
self.mount_address(params.address.as_str());
|
||||
self.mount_port(params.port);
|
||||
self.mount_username(params.username.as_deref().unwrap_or(""));
|
||||
self.mount_password(params.password.as_deref().unwrap_or(""));
|
||||
/// Load bookmark data into the gui components
|
||||
fn load_remote_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
|
||||
// Load parameters into components
|
||||
self.remote_protocol = bookmark.protocol;
|
||||
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) {
|
||||
self.mount_s3_bucket(params.bucket_name.as_str());
|
||||
self.mount_s3_region(params.region.as_deref().unwrap_or(""));
|
||||
self.mount_s3_endpoint(params.endpoint.as_deref().unwrap_or(""));
|
||||
self.mount_s3_profile(params.profile.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_generic_into_gui(&mut self, form_tab: FormTab, params: GenericProtocolParams) {
|
||||
self.mount_address(form_tab, params.address.as_str());
|
||||
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(""));
|
||||
}
|
||||
|
||||
fn load_bookmark_kube_into_gui(&mut self, params: KubeProtocolParams) {
|
||||
self.mount_kube_cluster_url(params.cluster_url.as_deref().unwrap_or(""));
|
||||
self.mount_kube_namespace(params.namespace.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_cert(params.client_cert.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_key(params.client_key.as_deref().unwrap_or(""));
|
||||
self.mount_kube_username(params.username.as_deref().unwrap_or(""));
|
||||
fn load_bookmark_s3_into_gui(&mut self, form_tab: FormTab, params: AwsS3Params) {
|
||||
self.mount_s3_bucket(form_tab, params.bucket_name.as_str());
|
||||
self.mount_s3_region(form_tab, params.region.as_deref().unwrap_or(""));
|
||||
self.mount_s3_endpoint(form_tab, params.endpoint.as_deref().unwrap_or(""));
|
||||
self.mount_s3_profile(form_tab, params.profile.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) {
|
||||
self.mount_address(params.address.as_str());
|
||||
fn load_bookmark_kube_into_gui(&mut self, form_tab: FormTab, params: KubeProtocolParams) {
|
||||
self.mount_kube_cluster_url(form_tab, params.cluster_url.as_deref().unwrap_or(""));
|
||||
self.mount_kube_namespace(form_tab, params.namespace.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_cert(form_tab, params.client_cert.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_key(form_tab, params.client_key.as_deref().unwrap_or(""));
|
||||
self.mount_kube_username(form_tab, params.username.as_deref().unwrap_or(""));
|
||||
}
|
||||
|
||||
fn load_bookmark_smb_into_gui(&mut self, form_tab: FormTab, params: SmbParams) {
|
||||
self.mount_address(form_tab, params.address.as_str());
|
||||
#[cfg(unix)]
|
||||
self.mount_port(params.port);
|
||||
self.mount_username(params.username.as_deref().unwrap_or(""));
|
||||
self.mount_password(params.password.as_deref().unwrap_or(""));
|
||||
self.mount_smb_share(¶ms.share);
|
||||
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, ¶ms.share);
|
||||
#[cfg(unix)]
|
||||
self.mount_smb_workgroup(params.workgroup.as_deref().unwrap_or(""));
|
||||
self.mount_smb_workgroup(form_tab, params.workgroup.as_deref().unwrap_or(""));
|
||||
}
|
||||
|
||||
fn load_bookmark_webdav_into_gui(&mut self, params: WebDAVProtocolParams) {
|
||||
self.mount_webdav_uri(¶ms.uri);
|
||||
self.mount_username(¶ms.username);
|
||||
self.mount_password(¶ms.password);
|
||||
fn load_bookmark_webdav_into_gui(&mut self, form_tab: FormTab, params: WebDAVProtocolParams) {
|
||||
self.mount_webdav_uri(form_tab, ¶ms.uri);
|
||||
self.mount_username(form_tab, ¶ms.username);
|
||||
self.mount_password(form_tab, ¶ms.password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputT
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
|
||||
use super::{FormMsg, Msg, UiMsg};
|
||||
use crate::ui::activities::auth::FormTab;
|
||||
|
||||
// -- bookmark list
|
||||
|
||||
@@ -323,10 +324,11 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
#[derive(MockComponent)]
|
||||
pub struct BookmarkSavePassword {
|
||||
component: Radio,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl BookmarkSavePassword {
|
||||
pub fn new(color: Color) -> Self {
|
||||
pub fn new(form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
@@ -340,6 +342,7 @@ impl BookmarkSavePassword {
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Save secrets?", Alignment::Center),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,7 +367,7 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark)),
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::SaveBookmarkPasswordBlur))
|
||||
}
|
||||
@@ -378,10 +381,11 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
#[derive(MockComponent)]
|
||||
pub struct BookmarkName {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl BookmarkName {
|
||||
pub fn new(color: Color) -> Self {
|
||||
pub fn new(form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
@@ -393,6 +397,7 @@ impl BookmarkName {
|
||||
.foreground(color)
|
||||
.title("Bookmark name", Alignment::Left)
|
||||
.input_type(InputType::Text),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,7 +452,7 @@ impl Component<Msg, NoUserEvent> for BookmarkName {
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark)),
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::Ui(UiMsg::BookmarkNameBlur)),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,11 +16,12 @@ pub use bookmarks::{
|
||||
#[cfg(unix)]
|
||||
pub use form::InputSmbWorkgroup;
|
||||
pub use form::{
|
||||
InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeNamespace,
|
||||
InputKubeUsername, InputLocalDirectory, InputPassword, InputPort, InputRemoteDirectory,
|
||||
InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile, InputS3Region,
|
||||
InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputSmbShare,
|
||||
InputUsername, InputWebDAVUri, ProtocolRadio, RadioS3NewPathStyle,
|
||||
HostBridgeProtocolRadio, InputAddress, InputKubeClientCert, InputKubeClientKey,
|
||||
InputKubeClusterUrl, InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword,
|
||||
InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint,
|
||||
InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
|
||||
InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle,
|
||||
RemoteProtocolRadio,
|
||||
};
|
||||
pub use popup::{
|
||||
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
||||
|
||||
@@ -100,6 +100,8 @@ impl HelpFooter {
|
||||
TextSpan::from(" Change field "),
|
||||
TextSpan::from("<TAB>").bold().fg(key_color),
|
||||
TextSpan::from(" Switch tab "),
|
||||
TextSpan::from("<BACKTAB>").bold().fg(key_color),
|
||||
TextSpan::from(" Switch form "),
|
||||
TextSpan::from("<ENTER>").bold().fg(key_color),
|
||||
TextSpan::from(" Submit form "),
|
||||
TextSpan::from("<F10|ESC>").bold().fg(key_color),
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
//!
|
||||
//! `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::HostBridgeParams;
|
||||
use crate::system::auto_update::{Release, Update, UpdateStatus};
|
||||
use crate::system::notifications::Notification;
|
||||
|
||||
@@ -36,24 +39,64 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// Collect host params as `FileTransferParams`
|
||||
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
match self.protocol {
|
||||
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(),
|
||||
FileTransferProtocol::Kube => self.collect_kube_host_params(),
|
||||
FileTransferProtocol::Smb => self.collect_smb_host_params(),
|
||||
pub(super) fn collect_host_bridge_params(&self) -> Result<HostBridgeParams, &'static str> {
|
||||
match self.host_bridge_protocol {
|
||||
HostBridgeProtocol::Localhost => self.collect_localhost_host_params(),
|
||||
HostBridgeProtocol::Remote(remote) => {
|
||||
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::Scp
|
||||
| FileTransferProtocol::Sftp => self.collect_generic_host_params(self.protocol),
|
||||
FileTransferProtocol::WebDAV => self.collect_webdav_host_params(),
|
||||
| FileTransferProtocol::Sftp => {
|
||||
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
|
||||
pub(super) fn collect_generic_host_params(
|
||||
&self,
|
||||
protocol: FileTransferProtocol,
|
||||
form_tab: FormTab,
|
||||
) -> 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() {
|
||||
return Err("Invalid host");
|
||||
}
|
||||
@@ -63,39 +106,48 @@ impl AuthActivity {
|
||||
Ok(FileTransferParams {
|
||||
protocol,
|
||||
params: ProtocolParams::Generic(params),
|
||||
local_path: self.get_input_local_directory(),
|
||||
remote_path: self.get_input_remote_directory(),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
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
|
||||
pub(super) fn collect_s3_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_s3_params_input();
|
||||
pub(super) fn collect_s3_host_params(
|
||||
&self,
|
||||
form_tab: FormTab,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_s3_params_input(form_tab);
|
||||
if params.bucket_name.is_empty() {
|
||||
return Err("Invalid bucket");
|
||||
}
|
||||
Ok(FileTransferParams {
|
||||
protocol: FileTransferProtocol::AwsS3,
|
||||
params: ProtocolParams::AwsS3(params),
|
||||
local_path: self.get_input_local_directory(),
|
||||
remote_path: self.get_input_remote_directory(),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
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
|
||||
pub(super) fn collect_kube_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_kube_params_input();
|
||||
pub(super) fn collect_kube_host_params(
|
||||
&self,
|
||||
form_tab: FormTab,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_kube_params_input(form_tab);
|
||||
|
||||
Ok(FileTransferParams {
|
||||
protocol: FileTransferProtocol::Kube,
|
||||
params: ProtocolParams::Kube(params),
|
||||
local_path: self.get_input_local_directory(),
|
||||
remote_path: self.get_input_remote_directory(),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
remote_path: self.get_input_remote_directory(form_tab),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn collect_smb_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_smb_params_input();
|
||||
pub(super) fn collect_smb_host_params(
|
||||
&self,
|
||||
form_tab: FormTab,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_smb_params_input(form_tab);
|
||||
if params.address.is_empty() {
|
||||
return Err("Invalid address");
|
||||
}
|
||||
@@ -109,21 +161,24 @@ impl AuthActivity {
|
||||
Ok(FileTransferParams {
|
||||
protocol: FileTransferProtocol::Smb,
|
||||
params: ProtocolParams::Smb(params),
|
||||
local_path: self.get_input_local_directory(),
|
||||
remote_path: self.get_input_remote_directory(),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
remote_path: self.get_input_remote_directory(form_tab),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn collect_webdav_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_webdav_params_input();
|
||||
pub(super) fn collect_webdav_host_params(
|
||||
&self,
|
||||
form_tab: FormTab,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_webdav_params_input(form_tab);
|
||||
if params.uri.is_empty() {
|
||||
return Err("Invalid URI");
|
||||
}
|
||||
Ok(FileTransferParams {
|
||||
protocol: FileTransferProtocol::WebDAV,
|
||||
params: ProtocolParams::WebDAV(params),
|
||||
local_path: self.get_input_local_directory(),
|
||||
remote_path: self.get_input_remote_directory(),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
remote_path: self.get_input_remote_directory(form_tab),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,26 +17,36 @@ use tuirealm::application::PollStrategy;
|
||||
use tuirealm::listener::EventListenerCfg;
|
||||
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::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
|
||||
// radio
|
||||
const RADIO_PROTOCOL_SFTP: usize = 0;
|
||||
const RADIO_PROTOCOL_SCP: usize = 1;
|
||||
const RADIO_PROTOCOL_FTP: usize = 2;
|
||||
const RADIO_PROTOCOL_FTPS: usize = 3;
|
||||
const RADIO_PROTOCOL_S3: usize = 4;
|
||||
const RADIO_PROTOCOL_KUBE: usize = 5;
|
||||
const RADIO_PROTOCOL_WEBDAV: usize = 6;
|
||||
const RADIO_PROTOCOL_SMB: usize = 7;
|
||||
// host bridge protocol radio
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST: usize = 0;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_SFTP: usize = 1;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_SCP: usize = 2;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_FTP: usize = 3;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_FTPS: usize = 4;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_S3: usize = 5;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_KUBE: usize = 6;
|
||||
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
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum Id {
|
||||
Address,
|
||||
BookmarkName,
|
||||
BookmarkSavePassword,
|
||||
BookmarksList,
|
||||
@@ -45,22 +55,33 @@ pub enum Id {
|
||||
ErrorPopup,
|
||||
GlobalListener,
|
||||
HelpFooter,
|
||||
HostBridge(AuthFormId),
|
||||
InfoPopup,
|
||||
InstallUpdatePopup,
|
||||
Keybindings,
|
||||
NewVersionChangelog,
|
||||
NewVersionDisclaimer,
|
||||
QuitPopup,
|
||||
RecentsList,
|
||||
Remote(AuthFormId),
|
||||
Subtitle,
|
||||
Title,
|
||||
WaitPopup,
|
||||
WindowSizeError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum AuthFormId {
|
||||
Address,
|
||||
KubeNamespace,
|
||||
KubeClusterUrl,
|
||||
KubeUsername,
|
||||
KubeClientCert,
|
||||
KubeClientKey,
|
||||
LocalDirectory,
|
||||
NewVersionChangelog,
|
||||
NewVersionDisclaimer,
|
||||
Password,
|
||||
Port,
|
||||
Protocol,
|
||||
QuitPopup,
|
||||
RecentsList,
|
||||
RemoteDirectory,
|
||||
S3AccessKey,
|
||||
S3Bucket,
|
||||
@@ -74,23 +95,19 @@ pub enum Id {
|
||||
SmbShare,
|
||||
#[cfg(unix)]
|
||||
SmbWorkgroup,
|
||||
Subtitle,
|
||||
Title,
|
||||
Username,
|
||||
WaitPopup,
|
||||
WebDAVUri,
|
||||
WindowSizeError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum Msg {
|
||||
enum Msg {
|
||||
Form(FormMsg),
|
||||
Ui(UiMsg),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum FormMsg {
|
||||
enum FormMsg {
|
||||
Connect,
|
||||
DeleteBookmark,
|
||||
DeleteRecent,
|
||||
@@ -98,15 +115,14 @@ pub enum FormMsg {
|
||||
InstallUpdate,
|
||||
LoadBookmark(usize),
|
||||
LoadRecent(usize),
|
||||
ProtocolChanged(FileTransferProtocol),
|
||||
HostBridgeProtocolChanged(HostBridgeProtocol),
|
||||
RemoteProtocolChanged(FileTransferProtocol),
|
||||
Quit,
|
||||
SaveBookmark,
|
||||
SaveBookmark(FormTab),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum UiMsg {
|
||||
AddressBlurDown,
|
||||
AddressBlurUp,
|
||||
BookmarksListBlur,
|
||||
BookmarksTabBlur,
|
||||
CloseDeleteBookmark,
|
||||
@@ -117,6 +133,25 @@ pub enum UiMsg {
|
||||
CloseKeybindingsPopup,
|
||||
CloseQuitPopup,
|
||||
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,
|
||||
KubeNamespaceBlurUp,
|
||||
KubeClusterUrlBlurDown,
|
||||
@@ -136,7 +171,6 @@ pub enum UiMsg {
|
||||
PortBlurUp,
|
||||
ProtocolBlurDown,
|
||||
ProtocolBlurUp,
|
||||
RececentsListBlur,
|
||||
RemoteDirectoryBlurDown,
|
||||
RemoteDirectoryBlurUp,
|
||||
S3AccessKeyBlurDown,
|
||||
@@ -163,19 +197,10 @@ pub enum UiMsg {
|
||||
SmbWorkgroupDown,
|
||||
#[cfg(unix)]
|
||||
SmbWorkgroupUp,
|
||||
BookmarkNameBlur,
|
||||
SaveBookmarkPasswordBlur,
|
||||
ShowDeleteBookmarkPopup,
|
||||
ShowDeleteRecentPopup,
|
||||
ShowKeybindingsPopup,
|
||||
ShowQuitPopup,
|
||||
ShowReleaseNotes,
|
||||
ShowSaveBookmarkPopup,
|
||||
UsernameBlurDown,
|
||||
UsernameBlurUp,
|
||||
WebDAVUriBlurDown,
|
||||
WebDAVUriBlurUp,
|
||||
WindowResized,
|
||||
}
|
||||
|
||||
/// Auth form input mask
|
||||
@@ -184,10 +209,23 @@ enum InputMask {
|
||||
Generic,
|
||||
AwsS3,
|
||||
Kube,
|
||||
Localhost,
|
||||
Smb,
|
||||
WebDAV,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum HostBridgeProtocol {
|
||||
Localhost,
|
||||
Remote(FileTransferProtocol),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum FormTab {
|
||||
HostBridge,
|
||||
Remote,
|
||||
}
|
||||
|
||||
// Store keys
|
||||
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
||||
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
|
||||
@@ -203,8 +241,11 @@ pub struct AuthActivity {
|
||||
exit_reason: Option<ExitReason>,
|
||||
/// Should redraw ui
|
||||
redraw: bool,
|
||||
/// Protocol
|
||||
protocol: FileTransferProtocol,
|
||||
/// Host bridge protocol
|
||||
host_bridge_protocol: HostBridgeProtocol,
|
||||
last_form_tab: FormTab,
|
||||
/// Remote file transfer protocol
|
||||
remote_protocol: FileTransferProtocol,
|
||||
context: Option<Context>,
|
||||
}
|
||||
|
||||
@@ -214,15 +255,17 @@ impl AuthActivity {
|
||||
AuthActivity {
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.default_input_listener(ticks)
|
||||
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL)
|
||||
.poll_timeout(ticks),
|
||||
),
|
||||
context: None,
|
||||
bookmarks_list: Vec::new(),
|
||||
exit_reason: None,
|
||||
last_form_tab: FormTab::Remote,
|
||||
recents_list: Vec::new(),
|
||||
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
|
||||
fn input_mask(&self) -> InputMask {
|
||||
match self.protocol {
|
||||
fn remote_input_mask(&self) -> InputMask {
|
||||
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::Ftp(_)
|
||||
| FileTransferProtocol::Scp
|
||||
@@ -275,7 +333,7 @@ impl Activity for AuthActivity {
|
||||
fn on_create(&mut self, mut context: Context) {
|
||||
debug!("Initializing activity");
|
||||
// Initialize file transfer params
|
||||
context.set_ftparams(FileTransferParams::default());
|
||||
context.set_remote_params(FileTransferParams::default());
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
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 {
|
||||
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
|
||||
@@ -21,19 +24,26 @@ impl AuthActivity {
|
||||
fn update_form(&mut self, msg: FormMsg) -> Option<Msg> {
|
||||
match msg {
|
||||
FormMsg::Connect => {
|
||||
match self.collect_host_params() {
|
||||
Err(err) => {
|
||||
// mount error
|
||||
self.mount_error(err);
|
||||
}
|
||||
Ok(params) => {
|
||||
self.save_recent();
|
||||
// Set file transfer params to context
|
||||
self.context_mut().set_ftparams(params);
|
||||
// Set exit reason
|
||||
self.exit_reason = Some(super::ExitReason::Connect);
|
||||
}
|
||||
}
|
||||
let Ok(remote_params) = self.collect_remote_host_params() else {
|
||||
// mount error
|
||||
self.mount_error("Invalid remote params parameters");
|
||||
return None;
|
||||
};
|
||||
|
||||
let Ok(host_bridge_params) = self.collect_host_bridge_params() else {
|
||||
// mount error
|
||||
self.mount_error("Invalid host bridge params parameters");
|
||||
return None;
|
||||
};
|
||||
|
||||
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 => {
|
||||
if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::BookmarksList) {
|
||||
@@ -62,50 +72,86 @@ impl AuthActivity {
|
||||
self.install_update();
|
||||
}
|
||||
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)
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::Password,
|
||||
InputMask::Smb => &Id::Password,
|
||||
InputMask::AwsS3 => &Id::S3Bucket,
|
||||
InputMask::Kube => &Id::KubeNamespace,
|
||||
InputMask::WebDAV => &Id::Password,
|
||||
})
|
||||
.is_ok());
|
||||
let focus = match self.last_form_tab {
|
||||
FormTab::Remote => match self.remote_input_mask() {
|
||||
InputMask::Localhost => &Id::Remote(AuthFormId::LocalDirectory),
|
||||
InputMask::Generic => &Id::Remote(AuthFormId::Password),
|
||||
InputMask::Smb => &Id::Remote(AuthFormId::Password),
|
||||
InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket),
|
||||
InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace),
|
||||
InputMask::WebDAV => &Id::Remote(AuthFormId::Password),
|
||||
},
|
||||
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) => {
|
||||
self.load_recent(i);
|
||||
self.load_recent(self.last_form_tab, i);
|
||||
// Give focus to input password (or to protocol if not generic)
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::Password,
|
||||
InputMask::Smb => &Id::Password,
|
||||
InputMask::AwsS3 => &Id::S3Bucket,
|
||||
InputMask::Kube => &Id::KubeNamespace,
|
||||
InputMask::WebDAV => &Id::Password,
|
||||
})
|
||||
.is_ok());
|
||||
let focus = match self.last_form_tab {
|
||||
FormTab::Remote => match self.remote_input_mask() {
|
||||
InputMask::Localhost => &Id::Remote(AuthFormId::LocalDirectory),
|
||||
InputMask::Generic => &Id::Remote(AuthFormId::Password),
|
||||
InputMask::Smb => &Id::Remote(AuthFormId::Password),
|
||||
InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket),
|
||||
InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace),
|
||||
InputMask::WebDAV => &Id::Remote(AuthFormId::Password),
|
||||
},
|
||||
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) => {
|
||||
self.protocol = protocol;
|
||||
FormMsg::HostBridgeProtocolChanged(protocol) => {
|
||||
self.host_bridge_protocol = protocol;
|
||||
// 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) {
|
||||
self.mount_port(Self::get_default_port_for_protocol(protocol));
|
||||
self.mount_port(
|
||||
FormTab::Remote,
|
||||
Self::get_default_port_for_protocol(protocol),
|
||||
);
|
||||
}
|
||||
}
|
||||
FormMsg::Quit => {
|
||||
self.exit_reason = Some(ExitReason::Quit);
|
||||
}
|
||||
FormMsg::SaveBookmark => {
|
||||
FormMsg::SaveBookmark(form_tab) => {
|
||||
// get bookmark name
|
||||
let (name, save_password) = self.get_new_bookmark();
|
||||
// Save bookmark
|
||||
if !name.is_empty() {
|
||||
self.save_bookmark(name, save_password);
|
||||
self.save_bookmark(form_tab, name, save_password);
|
||||
}
|
||||
// Umount popup
|
||||
self.umount_bookmark_save_dialog();
|
||||
@@ -118,16 +164,30 @@ impl AuthActivity {
|
||||
|
||||
fn update_ui(&mut self, msg: UiMsg) -> Option<Msg> {
|
||||
match msg {
|
||||
UiMsg::AddressBlurDown => {
|
||||
let id = if cfg!(windows) && self.input_mask() == InputMask::Smb {
|
||||
&Id::SmbShare
|
||||
UiMsg::HostBridge(UiAuthFormMsg::AddressBlurDown) => {
|
||||
let id = if cfg!(windows) && self.host_bridge_input_mask() == InputMask::Smb {
|
||||
&Id::HostBridge(AuthFormId::SmbShare)
|
||||
} else {
|
||||
&Id::Port
|
||||
&Id::HostBridge(AuthFormId::Port)
|
||||
};
|
||||
assert!(self.app.active(id).is_ok());
|
||||
}
|
||||
UiMsg::AddressBlurUp => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::AddressBlurDown) => {
|
||||
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 => {
|
||||
assert!(self.app.active(&Id::RecentsList).is_ok());
|
||||
@@ -136,7 +196,21 @@ impl AuthActivity {
|
||||
assert!(self.app.active(&Id::BookmarkSavePassword).is_ok());
|
||||
}
|
||||
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 => {
|
||||
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::BookmarkSavePassword).is_ok());
|
||||
}
|
||||
UiMsg::LocalDirectoryBlurDown => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::LocalDirectoryBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::Protocol))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::LocalDirectoryBlurUp => {
|
||||
assert!(self.app.active(&Id::RemoteDirectory).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurDown) => {
|
||||
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());
|
||||
}
|
||||
UiMsg::PasswordBlurDown => {
|
||||
UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur) => {
|
||||
assert!(self.app.active(&Id::BookmarksList).is_ok());
|
||||
}
|
||||
UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::RemoteDirectory,
|
||||
.active(match self.host_bridge_input_mask() {
|
||||
InputMask::Localhost => unreachable!(),
|
||||
InputMask::Generic => &Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||
#[cfg(unix)]
|
||||
InputMask::Smb => &Id::SmbWorkgroup,
|
||||
InputMask::Smb => &Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||
#[cfg(windows)]
|
||||
InputMask::Smb => &Id::RemoteDirectory,
|
||||
InputMask::AwsS3 => panic!("this shouldn't happen (password on s3)"),
|
||||
InputMask::Kube => panic!("this shouldn't happen (password on kube)"),
|
||||
InputMask::WebDAV => &Id::RemoteDirectory,
|
||||
InputMask::Smb => &Id::HostBridge(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::HostBridge(AuthFormId::RemoteDirectory),
|
||||
})
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::PasswordBlurUp => {
|
||||
assert!(self.app.active(&Id::Username).is_ok());
|
||||
}
|
||||
UiMsg::PortBlurDown => {
|
||||
UiMsg::Remote(UiAuthFormMsg::PasswordBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::Username,
|
||||
InputMask::Smb => &Id::SmbShare,
|
||||
InputMask::AwsS3 | InputMask::Kube | InputMask::WebDAV =>
|
||||
panic!("this shouldn't happen (port on s3/kube/webdav)"),
|
||||
.active(match self.remote_input_mask() {
|
||||
InputMask::Localhost => unreachable!(),
|
||||
InputMask::Generic => &Id::Remote(AuthFormId::RemoteDirectory),
|
||||
#[cfg(unix)]
|
||||
InputMask::Smb => &Id::Remote(AuthFormId::SmbWorkgroup),
|
||||
#[cfg(windows)]
|
||||
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());
|
||||
}
|
||||
UiMsg::PortBlurUp => {
|
||||
assert!(self.app.active(&Id::Address).is_ok());
|
||||
}
|
||||
UiMsg::ProtocolBlurDown => {
|
||||
UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::Address,
|
||||
InputMask::Smb => &Id::Address,
|
||||
InputMask::AwsS3 => &Id::S3Bucket,
|
||||
InputMask::Kube => &Id::KubeNamespace,
|
||||
InputMask::WebDAV => &Id::WebDAVUri,
|
||||
.active(&Id::HostBridge(AuthFormId::Username))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::Remote(UiAuthFormMsg::PasswordBlurUp) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::Username)).is_ok());
|
||||
}
|
||||
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());
|
||||
}
|
||||
UiMsg::ProtocolBlurUp => {
|
||||
assert!(self.app.active(&Id::LocalDirectory).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::PortBlurDown) => {
|
||||
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 => {
|
||||
assert!(self.app.active(&Id::BookmarksList).is_ok());
|
||||
}
|
||||
UiMsg::RemoteDirectoryBlurDown => {
|
||||
assert!(self.app.active(&Id::LocalDirectory).is_ok());
|
||||
}
|
||||
UiMsg::RemoteDirectoryBlurUp => {
|
||||
UiMsg::HostBridge(UiAuthFormMsg::RemoteDirectoryBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::Password,
|
||||
.active(&Id::HostBridge(AuthFormId::LocalDirectory))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::Remote(AuthFormId::LocalDirectory))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::HostBridge(UiAuthFormMsg::RemoteDirectoryBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.host_bridge_input_mask() {
|
||||
InputMask::Localhost => unreachable!(),
|
||||
InputMask::Generic => &Id::HostBridge(AuthFormId::Password),
|
||||
#[cfg(unix)]
|
||||
InputMask::Smb => &Id::SmbWorkgroup,
|
||||
InputMask::Smb => &Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||
#[cfg(windows)]
|
||||
InputMask::Smb => &Id::Password,
|
||||
InputMask::Kube => &Id::KubeClientKey,
|
||||
InputMask::AwsS3 => &Id::S3NewPathStyle,
|
||||
InputMask::WebDAV => &Id::Password,
|
||||
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());
|
||||
}
|
||||
UiMsg::S3BucketBlurDown => {
|
||||
assert!(self.app.active(&Id::S3Region).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.remote_input_mask() {
|
||||
InputMask::Localhost => unreachable!(),
|
||||
InputMask::Generic => &Id::Remote(AuthFormId::Password),
|
||||
#[cfg(unix)]
|
||||
InputMask::Smb => &Id::Remote(AuthFormId::SmbWorkgroup),
|
||||
#[cfg(windows)]
|
||||
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 => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3Region))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::S3RegionBlurDown => {
|
||||
assert!(self.app.active(&Id::S3Endpoint).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3BucketBlurDown) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::S3Region)).is_ok());
|
||||
}
|
||||
UiMsg::S3RegionBlurUp => {
|
||||
assert!(self.app.active(&Id::S3Bucket).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::Protocol))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::S3EndpointBlurDown => {
|
||||
assert!(self.app.active(&Id::S3Profile).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3BucketBlurUp) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok());
|
||||
}
|
||||
UiMsg::S3EndpointBlurUp => {
|
||||
assert!(self.app.active(&Id::S3Region).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3Endpoint))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::S3ProfileBlurDown => {
|
||||
assert!(self.app.active(&Id::S3AccessKey).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3RegionBlurDown) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::S3Endpoint)).is_ok());
|
||||
}
|
||||
UiMsg::S3ProfileBlurUp => {
|
||||
assert!(self.app.active(&Id::S3Endpoint).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3Bucket))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::S3AccessKeyBlurDown => {
|
||||
assert!(self.app.active(&Id::S3SecretAccessKey).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3RegionBlurUp) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::S3Bucket)).is_ok());
|
||||
}
|
||||
UiMsg::S3AccessKeyBlurUp => {
|
||||
assert!(self.app.active(&Id::S3Profile).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3Profile))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::S3SecretAccessKeyBlurDown => {
|
||||
assert!(self.app.active(&Id::S3SecurityToken).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurDown) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::S3Profile)).is_ok());
|
||||
}
|
||||
UiMsg::S3SecretAccessKeyBlurUp => {
|
||||
assert!(self.app.active(&Id::S3AccessKey).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3Region))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::S3SecurityTokenBlurDown => {
|
||||
assert!(self.app.active(&Id::S3SessionToken).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurUp) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::S3Region)).is_ok());
|
||||
}
|
||||
UiMsg::S3SecurityTokenBlurUp => {
|
||||
assert!(self.app.active(&Id::S3SecretAccessKey).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3AccessKey))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::S3SessionTokenBlurDown => {
|
||||
assert!(self.app.active(&Id::S3NewPathStyle).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::Remote(AuthFormId::S3AccessKey))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::S3SessionTokenBlurUp => {
|
||||
assert!(self.app.active(&Id::S3SecurityToken).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3Endpoint))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::S3NewPathStyleBlurDown => {
|
||||
assert!(self.app.active(&Id::RemoteDirectory).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurUp) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::S3Endpoint)).is_ok());
|
||||
}
|
||||
UiMsg::S3NewPathStyleBlurUp => {
|
||||
assert!(self.app.active(&Id::S3SessionToken).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3SecretAccessKey))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::KubeClientCertBlurDown => {
|
||||
assert!(self.app.active(&Id::KubeClientKey).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::Remote(AuthFormId::S3SecretAccessKey))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::KubeClientCertBlurUp => {
|
||||
assert!(self.app.active(&Id::KubeUsername).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3Profile))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::KubeClientKeyBlurDown => {
|
||||
assert!(self.app.active(&Id::RemoteDirectory).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurUp) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::S3Profile)).is_ok());
|
||||
}
|
||||
UiMsg::KubeClientKeyBlurUp => {
|
||||
assert!(self.app.active(&Id::KubeClientCert).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3SecretAccessKeyBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3SecurityToken))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::KubeNamespaceBlurDown => {
|
||||
assert!(self.app.active(&Id::KubeClusterUrl).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::Remote(AuthFormId::S3SecurityToken))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::KubeNamespaceBlurUp => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3SecretAccessKeyBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3AccessKey))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::KubeClusterUrlBlurDown => {
|
||||
assert!(self.app.active(&Id::KubeUsername).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::Remote(AuthFormId::S3AccessKey))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::KubeClusterUrlBlurUp => {
|
||||
assert!(self.app.active(&Id::KubeNamespace).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3SecurityTokenBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3SessionToken))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::KubeUsernameBlurDown => {
|
||||
assert!(self.app.active(&Id::KubeClientCert).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::Remote(AuthFormId::S3SessionToken))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::KubeUsernameBlurUp => {
|
||||
assert!(self.app.active(&Id::KubeClusterUrl).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3SecurityTokenBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::S3SecretAccessKey))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::SmbShareBlurDown => {
|
||||
assert!(self.app.active(&Id::Username).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::Remote(AuthFormId::S3SecretAccessKey))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::SmbShareBlurUp => {
|
||||
let id = if cfg!(windows) && self.input_mask() == InputMask::Smb {
|
||||
&Id::Address
|
||||
UiMsg::HostBridge(UiAuthFormMsg::S3SessionTokenBlurDown) => {
|
||||
assert!(self
|
||||
.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 {
|
||||
&Id::Port
|
||||
&Id::HostBridge(AuthFormId::Port)
|
||||
};
|
||||
assert!(self.app.active(id).is_ok());
|
||||
}
|
||||
UiMsg::Remote(UiAuthFormMsg::SmbShareBlurUp) => {
|
||||
let id = if cfg!(windows) && self.remote_input_mask() == InputMask::Smb {
|
||||
&Id::Remote(AuthFormId::Address)
|
||||
} else {
|
||||
&Id::Remote(AuthFormId::Port)
|
||||
};
|
||||
assert!(self.app.active(id).is_ok());
|
||||
}
|
||||
#[cfg(unix)]
|
||||
UiMsg::SmbWorkgroupDown => {
|
||||
assert!(self.app.active(&Id::RemoteDirectory).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::RemoteDirectory))
|
||||
.is_ok());
|
||||
}
|
||||
#[cfg(unix)]
|
||||
UiMsg::SmbWorkgroupUp => {
|
||||
assert!(self.app.active(&Id::Password).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::Remote(AuthFormId::RemoteDirectory))
|
||||
.is_ok());
|
||||
}
|
||||
#[cfg(unix)]
|
||||
UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupUp) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(&Id::HostBridge(AuthFormId::Password))
|
||||
.is_ok());
|
||||
}
|
||||
#[cfg(unix)]
|
||||
UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupUp) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::Password)).is_ok());
|
||||
}
|
||||
UiMsg::SaveBookmarkPasswordBlur => {
|
||||
assert!(self.app.active(&Id::BookmarkName).is_ok());
|
||||
@@ -361,28 +804,60 @@ impl AuthActivity {
|
||||
self.mount_release_notes();
|
||||
}
|
||||
UiMsg::ShowSaveBookmarkPopup => {
|
||||
self.mount_bookmark_save_dialog();
|
||||
self.mount_bookmark_save_dialog(self.get_current_form_tab());
|
||||
}
|
||||
UiMsg::UsernameBlurDown => {
|
||||
assert!(self.app.active(&Id::Password).is_ok());
|
||||
}
|
||||
UiMsg::UsernameBlurUp => {
|
||||
UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurDown) => {
|
||||
assert!(self
|
||||
.app
|
||||
.active(match self.input_mask() {
|
||||
InputMask::Generic => &Id::Port,
|
||||
InputMask::Smb => &Id::SmbShare,
|
||||
InputMask::Kube => panic!("this shouldn't happen (username on kube)"),
|
||||
InputMask::AwsS3 => panic!("this shouldn't happen (username on s3)"),
|
||||
InputMask::WebDAV => &Id::WebDAVUri,
|
||||
.active(&Id::HostBridge(AuthFormId::Password))
|
||||
.is_ok());
|
||||
}
|
||||
UiMsg::Remote(UiAuthFormMsg::UsernameBlurDown) => {
|
||||
assert!(self.app.active(&Id::Remote(AuthFormId::Password)).is_ok());
|
||||
}
|
||||
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());
|
||||
}
|
||||
UiMsg::WebDAVUriBlurDown => {
|
||||
assert!(self.app.active(&Id::Username).is_ok());
|
||||
UiMsg::Remote(UiAuthFormMsg::UsernameBlurUp) => {
|
||||
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 => {
|
||||
assert!(self.app.active(&Id::Protocol).is_ok());
|
||||
UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurDown) => {
|
||||
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 => {
|
||||
self.redraw = true;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ enum SyncBrowsingDestination {
|
||||
impl FileTransferActivity {
|
||||
/// Enter a directory on local host from entry
|
||||
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() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
||||
}
|
||||
@@ -35,8 +35,9 @@ impl FileTransferActivity {
|
||||
|
||||
/// Change local directory reading value from input
|
||||
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());
|
||||
self.local_changedir(dir_path.as_path(), true);
|
||||
let dir_path: PathBuf =
|
||||
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
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
||||
@@ -55,8 +56,8 @@ impl FileTransferActivity {
|
||||
|
||||
/// Go to previous directory from localhost
|
||||
pub(crate) fn action_go_to_previous_local_dir(&mut self) {
|
||||
if let Some(d) = self.local_mut().popd() {
|
||||
self.local_changedir(d.as_path(), false);
|
||||
if let Some(d) = self.host_bridge_mut().popd() {
|
||||
self.host_bridge_changedir(d.as_path(), false);
|
||||
// Check whether to sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
||||
@@ -78,10 +79,10 @@ impl FileTransferActivity {
|
||||
/// Go to upper directory on local host
|
||||
pub(crate) fn action_go_to_local_upper_dir(&mut self) {
|
||||
// Get pwd
|
||||
let path: PathBuf = self.local().wrkdir.clone();
|
||||
let path: PathBuf = self.host_bridge().wrkdir.clone();
|
||||
// Go to parent directory
|
||||
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 self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
||||
@@ -118,7 +119,7 @@ impl FileTransferActivity {
|
||||
trace!("Synchronizing browsing to path {}", path.display());
|
||||
// Check whether destination exists on host
|
||||
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,
|
||||
Err(err) => {
|
||||
error!(
|
||||
@@ -129,7 +130,17 @@ impl FileTransferActivity {
|
||||
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,
|
||||
};
|
||||
let name = path
|
||||
@@ -150,7 +161,7 @@ impl FileTransferActivity {
|
||||
trace!("User wants to create the unexisting directory");
|
||||
// Make directory
|
||||
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()),
|
||||
_ => {}
|
||||
}
|
||||
@@ -173,18 +184,18 @@ impl FileTransferActivity {
|
||||
// Enter directory
|
||||
match destination {
|
||||
SyncBrowsingDestination::ParentDir => match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.local_changedir(path.as_path(), true),
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
||||
_ => {}
|
||||
},
|
||||
SyncBrowsingDestination::Path(_) => match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.local_changedir(path.as_path(), true),
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
||||
_ => {}
|
||||
},
|
||||
SyncBrowsingDestination::PreviousDir => match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.remote_changedir(path.as_path(), false),
|
||||
FileExplorerTab::Remote => self.local_changedir(path.as_path(), false),
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), false),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), false),
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
@@ -197,13 +208,13 @@ impl FileTransferActivity {
|
||||
) -> Option<PathBuf> {
|
||||
match (destination, self.browser.tab()) {
|
||||
// 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())
|
||||
}
|
||||
(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() {
|
||||
Some(p)
|
||||
} else {
|
||||
@@ -212,7 +223,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => {
|
||||
if let Some(p) = self.local_mut().popd() {
|
||||
if let Some(p) = self.host_bridge_mut().popd() {
|
||||
Some(p)
|
||||
} else {
|
||||
warn!("Cannot synchronize browsing: local has no previous directory in stack");
|
||||
|
||||
@@ -3,12 +3,11 @@ use remotefs::fs::UnixPex;
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
#[cfg(unix)]
|
||||
pub fn action_local_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_local_selected_entries().get_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(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
@@ -51,12 +50,11 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn action_find_local_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_found_selected_entries().get_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(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
|
||||
@@ -53,7 +53,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
fn local_copy_file(&mut self, entry: &File, dest: &Path) {
|
||||
match self.host.copy(entry, dest) {
|
||||
match self.host_bridge.copy(entry, dest) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
@@ -136,7 +136,7 @@ impl FileTransferActivity {
|
||||
return Err(err);
|
||||
}
|
||||
// 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,
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
@@ -189,7 +189,7 @@ impl FileTransferActivity {
|
||||
return Err(err);
|
||||
}
|
||||
// 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(_) => panic!("{} is not a file", tmpfile.path().display()),
|
||||
Err(err) => {
|
||||
|
||||
@@ -43,7 +43,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
pub(crate) fn local_remove_file(&mut self, entry: &File) {
|
||||
match self.host.remove(entry) {
|
||||
match self.host_bridge.remove(entry) {
|
||||
Ok(_) => {
|
||||
// Log
|
||||
self.log(
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
// ext
|
||||
use remotefs::fs::Metadata;
|
||||
use remotefs::File;
|
||||
|
||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||
@@ -29,7 +28,12 @@ impl FileTransferActivity {
|
||||
format!("Opening file \"{}\"…", entry.path().display()),
|
||||
);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +63,83 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
/// 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
|
||||
match OpenOptions::new().read(true).open(path) {
|
||||
Ok(mut f) => {
|
||||
@@ -90,6 +170,8 @@ impl FileTransferActivity {
|
||||
}
|
||||
// Lock ports
|
||||
assert!(self.app.lock_ports().is_ok());
|
||||
// Get current file modification time
|
||||
let prev_mtime = self.get_localhost_mtime(path)?;
|
||||
// Open editor
|
||||
match edit::edit_file(path) {
|
||||
Ok(_) => self.log(
|
||||
@@ -117,7 +199,23 @@ impl FileTransferActivity {
|
||||
// Unlock ports
|
||||
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
|
||||
@@ -138,7 +236,7 @@ impl FileTransferActivity {
|
||||
return Err(format!("Could not open file {file_name}: {err}"));
|
||||
}
|
||||
// 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),
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
@@ -151,7 +249,7 @@ impl FileTransferActivity {
|
||||
// Edit file
|
||||
self.edit_local_file(tmpfile.as_path())?;
|
||||
// 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,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
@@ -177,7 +275,7 @@ impl FileTransferActivity {
|
||||
),
|
||||
);
|
||||
// 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,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
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) => {
|
||||
// Reload files
|
||||
self.log(LogLevel::Info, format!("\"{input}\": {output}"));
|
||||
|
||||
@@ -40,7 +40,7 @@ impl FileTransferActivity {
|
||||
let filter = Filter::from_str(filter).unwrap();
|
||||
|
||||
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(),
|
||||
_ => return vec![],
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ impl FileTransferActivity {
|
||||
};
|
||||
// Change directory
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||
self.local_changedir(path.as_path(), true)
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.host_bridge_changedir(path.as_path(), true)
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
self.remote_changedir(path.as_path(), true)
|
||||
@@ -36,12 +36,16 @@ impl FileTransferActivity {
|
||||
|
||||
pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
|
||||
let wrkdir: PathBuf = match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(),
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(),
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.remote().wrkdir.clone()
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
self.host_bridge().wrkdir.clone()
|
||||
}
|
||||
};
|
||||
match self.get_found_selected_entries() {
|
||||
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());
|
||||
if self.config().get_prompt_on_file_replace()
|
||||
&& self.remote_file_exists(file_to_check.as_path())
|
||||
@@ -66,7 +70,7 @@ impl FileTransferActivity {
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||
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(
|
||||
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
||||
)
|
||||
@@ -94,7 +98,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
// Iter files
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
if self.config().get_prompt_on_file_replace() {
|
||||
// Check which file would be replaced
|
||||
let existing_files: Vec<&File> = entries
|
||||
@@ -131,7 +135,7 @@ impl FileTransferActivity {
|
||||
let existing_files: Vec<&File> = entries
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
self.local_file_exists(
|
||||
self.host_bridge_file_exists(
|
||||
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) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.local_remove_file(entry);
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
@@ -224,7 +228,7 @@ impl FileTransferActivity {
|
||||
|
||||
fn open_found_file(&mut self, entry: &File, with: Option<&str>) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.action_open_local_file(entry, with);
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
|
||||
@@ -11,7 +11,10 @@ use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
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(_) => {
|
||||
// Reload files
|
||||
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
||||
|
||||
@@ -86,12 +86,12 @@ impl From<Vec<&File>> for SelectedFile {
|
||||
impl FileTransferActivity {
|
||||
/// Get local file entry
|
||||
pub(crate) fn get_local_selected_entries(&self) -> SelectedFile {
|
||||
match self.get_selected_index(&Id::ExplorerLocal) {
|
||||
SelectedFileIndex::One(idx) => SelectedFile::from(self.local().get(idx)),
|
||||
match self.get_selected_index(&Id::ExplorerHostBridge) {
|
||||
SelectedFileIndex::One(idx) => SelectedFile::from(self.host_bridge().get(idx)),
|
||||
SelectedFileIndex::Many(files) => {
|
||||
let files: Vec<&File> = files
|
||||
.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();
|
||||
SelectedFile::from(files)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
use std::fs::File as StdFile;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use remotefs::fs::Metadata;
|
||||
|
||||
use super::{File, FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_newfile(&mut self, input: String) {
|
||||
// Check if file exists
|
||||
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() {
|
||||
file_exists = true;
|
||||
}
|
||||
@@ -21,19 +23,35 @@ impl FileTransferActivity {
|
||||
self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create file
|
||||
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(
|
||||
LogLevel::Error,
|
||||
format!("Could not create file \"{}\": {}", file_path.display(), err),
|
||||
);
|
||||
} else {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Created file \"{}\"", file_path.display()),
|
||||
format!("Could not write file \"{}\": {}", file_path.display(), err),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Created file \"{}\"", file_path.display()),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn action_remote_newfile(&mut self, input: String) {
|
||||
@@ -57,7 +75,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
Ok(tfile) => {
|
||||
// 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) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
|
||||
@@ -35,7 +35,11 @@ impl FileTransferActivity {
|
||||
|
||||
/// Perform open lopcal file
|
||||
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
|
||||
@@ -104,6 +108,57 @@ impl FileTransferActivity {
|
||||
.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.
|
||||
fn open_path_with(&mut self, p: &Path, with: Option<&str>) {
|
||||
// Open file
|
||||
|
||||
@@ -51,7 +51,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
fn local_rename_file(&mut self, entry: &File, dest: &Path) {
|
||||
match self.host.rename(entry, dest) {
|
||||
match self.host_bridge.rename(entry, dest) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
|
||||
@@ -93,12 +93,12 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
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() {
|
||||
SelectedFile::One(entry) => {
|
||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||
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(opts.save_as.clone().unwrap_or_else(|| entry.name()))
|
||||
{
|
||||
@@ -129,7 +129,7 @@ impl FileTransferActivity {
|
||||
let existing_files: Vec<&File> = entries
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
self.local_file_exists(
|
||||
self.host_bridge_file_exists(
|
||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_scan(&mut self, p: &Path) -> Result<Vec<File>, String> {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => self
|
||||
.host
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => self
|
||||
.host_bridge
|
||||
.list_dir(p)
|
||||
.map_err(|e| format!("Failed to list directory: {}", e)),
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self
|
||||
|
||||
@@ -19,7 +19,7 @@ impl FileTransferActivity {
|
||||
} else if entry.metadata().symlink.is_some() {
|
||||
// Stat file
|
||||
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,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
|
||||
@@ -9,11 +9,10 @@ use super::{FileTransferActivity, LogLevel, SelectedFile};
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Create symlink on localhost
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn action_local_symlink(&mut self, name: String) {
|
||||
if let SelectedFile::One(entry) = self.get_local_selected_entries() {
|
||||
match self
|
||||
.host
|
||||
.host_bridge
|
||||
.symlink(PathBuf::from(name.as_str()).as_path(), entry.path())
|
||||
{
|
||||
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
|
||||
pub(crate) fn action_remote_symlink(&mut self, name: String) {
|
||||
if let SelectedFile::One(entry) = self.get_remote_selected_entries() {
|
||||
|
||||
@@ -18,8 +18,16 @@ impl FileTransferActivity {
|
||||
pub(crate) fn action_walkdir_local(&mut self) -> Result<Vec<File>, WalkdirError> {
|
||||
let mut acc = Vec::with_capacity(32_768);
|
||||
|
||||
self.walkdir(&mut acc, &self.host.pwd(), |activity, path| {
|
||||
activity.host.list_dir(path).map_err(|e| e.to_string())
|
||||
let pwd = self
|
||||
.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)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent};
|
||||
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table};
|
||||
use tuirealm::tui::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 super::{Msg, UiMsg};
|
||||
@@ -32,7 +32,7 @@ impl 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 focus = self
|
||||
.props
|
||||
|
||||
@@ -17,7 +17,7 @@ use tuirealm::props::{
|
||||
};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
#[cfg(unix)]
|
||||
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::goto::{GotoPopup, ATTR_FILES};
|
||||
@@ -1036,7 +1036,7 @@ pub struct 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 {
|
||||
component: ProgressBar::default()
|
||||
.borders(
|
||||
@@ -1074,7 +1074,7 @@ pub struct 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 {
|
||||
component: ProgressBar::default()
|
||||
.borders(
|
||||
@@ -1551,8 +1551,8 @@ pub struct StatusBarLocal {
|
||||
|
||||
impl StatusBarLocal {
|
||||
pub fn new(browser: &Browser, sorting_color: Color, hidden_color: Color) -> Self {
|
||||
let file_sorting = file_sorting_label(browser.local().file_sorting);
|
||||
let hidden_files = hidden_files_label(browser.local().hidden_files_visible());
|
||||
let file_sorting = file_sorting_label(browser.host_bridge().file_sorting);
|
||||
let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible());
|
||||
Self {
|
||||
component: Span::default().spans(&[
|
||||
TextSpan::new("File sorting: ").fg(sorting_color),
|
||||
|
||||
@@ -3,7 +3,7 @@ use tui_realm_stdlib::Checkbox;
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction};
|
||||
use tuirealm::event::{Key, KeyEvent};
|
||||
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 super::{Msg, TransferMsg, UiMsg};
|
||||
@@ -186,7 +186,7 @@ impl MockComponent for ChmodPopup {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ impl 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
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::tui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
|
||||
use tuirealm::ratatui::text::{Line, Span};
|
||||
use tuirealm::ratatui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
|
||||
use tuirealm::{MockComponent, Props, State, StateValue};
|
||||
|
||||
pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
|
||||
pub const FILE_LIST_CMD_DESELECT_ALL: &str = "D";
|
||||
const PROP_DOT_DOT: &str = "dot_dot";
|
||||
|
||||
/// OwnStates contains states for this component
|
||||
#[derive(Clone, Default)]
|
||||
@@ -22,8 +23,8 @@ struct OwnStates {
|
||||
|
||||
impl OwnStates {
|
||||
/// Initialize list states
|
||||
pub fn init_list_states(&mut self, len: usize) {
|
||||
self.selected = Vec::with_capacity(len);
|
||||
pub fn init_list_states(&mut self, len: usize, has_dot_dot: bool) {
|
||||
self.selected = Vec::with_capacity(len + if has_dot_dot { 1 } else { 0 });
|
||||
self.fix_list_index();
|
||||
}
|
||||
|
||||
@@ -107,9 +108,9 @@ impl OwnStates {
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
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
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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
|
||||
.props
|
||||
.get_or(
|
||||
@@ -193,25 +208,42 @@ impl MockComponent for FileList {
|
||||
.unwrap_flag();
|
||||
let div = tui_realm_stdlib::utils::get_block(borders, Some(title), focus, None);
|
||||
// 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
|
||||
.props
|
||||
.get(Attribute::Content)
|
||||
.map(|x| x.unwrap_table())
|
||||
{
|
||||
Some(table) => table
|
||||
Some(table) => init_table_iter
|
||||
.iter()
|
||||
.chain(table.iter())
|
||||
.enumerate()
|
||||
.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
|
||||
.iter()
|
||||
.map(|col| {
|
||||
let (fg, bg, mut modifiers) =
|
||||
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
|
||||
| TextModifiers::UNDERLINED
|
||||
| TextModifiers::ITALIC;
|
||||
}
|
||||
|
||||
Span::styled(
|
||||
col.content.clone(),
|
||||
Style::default().add_modifier(modifiers).fg(fg).bg(bg),
|
||||
@@ -255,6 +287,7 @@ impl MockComponent for FileList {
|
||||
Some(line) => line.len(),
|
||||
_ => 0,
|
||||
},
|
||||
self.has_dot_dot(),
|
||||
);
|
||||
self.states.fix_list_index();
|
||||
}
|
||||
@@ -265,8 +298,16 @@ impl MockComponent for FileList {
|
||||
}
|
||||
|
||||
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() {
|
||||
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(
|
||||
self.states
|
||||
.get_selection()
|
||||
@@ -334,7 +375,7 @@ impl MockComponent for FileList {
|
||||
}
|
||||
}
|
||||
Cmd::Custom(FILE_LIST_CMD_SELECT_ALL) => {
|
||||
self.states.select_all();
|
||||
self.states.select_all(self.has_dot_dot());
|
||||
CmdResult::None
|
||||
}
|
||||
Cmd::Custom(FILE_LIST_CMD_DESELECT_ALL) => {
|
||||
@@ -342,7 +383,15 @@ impl MockComponent for FileList {
|
||||
CmdResult::None
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tui_realm_stdlib::Input;
|
||||
use tuirealm::command::{Cmd, CmdResult};
|
||||
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Table};
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::{MockComponent, State};
|
||||
|
||||
use super::file_list::FileList;
|
||||
@@ -88,7 +88,7 @@ impl 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
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
|
||||
@@ -363,7 +363,8 @@ impl ExplorerLocal {
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.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)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
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 {
|
||||
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 {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
@@ -559,7 +572,8 @@ impl ExplorerRemote {
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.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)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
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 {
|
||||
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 {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
|
||||
@@ -30,10 +30,10 @@ impl FileTransferActivity {
|
||||
Ok(Some(FsChange::Update(update))) => {
|
||||
debug!(
|
||||
"fs watcher reported an `Update` from {} to {}",
|
||||
update.local().display(),
|
||||
update.host_bridge().display(),
|
||||
update.remote().display()
|
||||
);
|
||||
self.upload_watched_file(update.local(), update.remote());
|
||||
self.upload_watched_file(update.host_bridge(), update.remote());
|
||||
}
|
||||
Err(err) => {
|
||||
self.log(
|
||||
@@ -87,9 +87,9 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_watched_file(&mut self, local: &Path, remote: &Path) {
|
||||
// stat local file
|
||||
let entry = match self.host.stat(local) {
|
||||
fn upload_watched_file(&mut self, host: &Path, remote: &Path) {
|
||||
// stat host file
|
||||
let entry = match self.host_bridge.stat(host) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
self.log(
|
||||
@@ -105,8 +105,8 @@ impl FileTransferActivity {
|
||||
};
|
||||
// send
|
||||
trace!(
|
||||
"syncing local file {} with remote {}",
|
||||
local.display(),
|
||||
"syncing host file {} with remote {}",
|
||||
host.display(),
|
||||
remote.display()
|
||||
);
|
||||
let remote_path = remote.parent().unwrap_or_else(|| Path::new("/"));
|
||||
@@ -116,7 +116,7 @@ impl FileTransferActivity {
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"synched watched file {} with {}",
|
||||
local.display(),
|
||||
host.display(),
|
||||
remote.display()
|
||||
),
|
||||
);
|
||||
|
||||
@@ -16,10 +16,10 @@ const FUZZY_SEARCH_THRESHOLD: u16 = 50;
|
||||
/// File explorer tab
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileExplorerTab {
|
||||
Local,
|
||||
HostBridge,
|
||||
Remote,
|
||||
FindLocal, // Find result tab
|
||||
FindRemote, // Find result tab
|
||||
FindHostBridge, // Find result tab
|
||||
FindRemote, // Find result tab
|
||||
}
|
||||
|
||||
/// Describes the explorer tab type
|
||||
@@ -31,10 +31,10 @@ pub enum FoundExplorerTab {
|
||||
|
||||
/// Browser contains the browser options
|
||||
pub struct Browser {
|
||||
local: FileExplorer, // Local File explorer state
|
||||
remote: FileExplorer, // Remote File explorer state
|
||||
found: Option<Found>, // File explorer for find result
|
||||
tab: FileExplorerTab, // Current selected tab
|
||||
host_bridge: FileExplorer, // Local File explorer state
|
||||
remote: FileExplorer, // Remote File explorer state
|
||||
found: Option<Found>, // File explorer for find result
|
||||
tab: FileExplorerTab, // Current selected tab
|
||||
pub sync_browsing: bool,
|
||||
}
|
||||
|
||||
@@ -42,30 +42,30 @@ impl Browser {
|
||||
/// Build a new `Browser` struct
|
||||
pub fn new(cli: &ConfigClient) -> Self {
|
||||
Self {
|
||||
local: Self::build_local_explorer(cli),
|
||||
host_bridge: Self::build_local_explorer(cli),
|
||||
remote: Self::build_remote_explorer(cli),
|
||||
found: None,
|
||||
tab: FileExplorerTab::Local,
|
||||
tab: FileExplorerTab::HostBridge,
|
||||
sync_browsing: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn explorer(&self) -> &FileExplorer {
|
||||
match self.tab {
|
||||
FileExplorerTab::Local => &self.local,
|
||||
FileExplorerTab::HostBridge => &self.host_bridge,
|
||||
FileExplorerTab::Remote => &self.remote,
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
self.found.as_ref().map(|x| &x.explorer).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local(&self) -> &FileExplorer {
|
||||
&self.local
|
||||
pub fn host_bridge(&self) -> &FileExplorer {
|
||||
&self.host_bridge
|
||||
}
|
||||
|
||||
pub fn local_mut(&mut self) -> &mut FileExplorer {
|
||||
&mut self.local
|
||||
pub fn host_bridge_mut(&mut self) -> &mut FileExplorer {
|
||||
&mut self.host_bridge
|
||||
}
|
||||
|
||||
pub fn remote(&self) -> &FileExplorer {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Locals
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use tuirealm::props::{
|
||||
Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextSpan,
|
||||
@@ -11,7 +9,7 @@ use tuirealm::{PollStrategy, Update};
|
||||
|
||||
use super::browser::FileExplorerTab;
|
||||
use super::{ConfigClient, FileTransferActivity, Id, LogLevel, LogRecord, TransferPayload};
|
||||
use crate::filetransfer::ProtocolParams;
|
||||
use crate::filetransfer::{HostBridgeParams, ProtocolParams};
|
||||
use crate::system::environment;
|
||||
use crate::system::notifications::Notification;
|
||||
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());
|
||||
}
|
||||
|
||||
/// Convert a path to absolute according to local explorer
|
||||
pub(super) fn local_to_abs_path(&self, path: &Path) -> PathBuf {
|
||||
path::absolutize(self.local().wrkdir.as_path(), path)
|
||||
/// Convert a path to absolute according to host explorer
|
||||
pub(super) fn host_bridge_to_abs_path(&self, path: &Path) -> PathBuf {
|
||||
path::absolutize(self.host_bridge().wrkdir.as_path(), path)
|
||||
}
|
||||
|
||||
/// Convert a path to absolute according to remote explorer
|
||||
@@ -107,8 +105,28 @@ impl FileTransferActivity {
|
||||
|
||||
/// Get remote hostname
|
||||
pub(super) fn get_remote_hostname(&self) -> String {
|
||||
let ft_params = self.context().ft_params().unwrap();
|
||||
match &ft_params.params {
|
||||
let ft_params = self.context().remote_params().unwrap();
|
||||
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::AwsS3(params) => params.bucket_name.clone(),
|
||||
ProtocolParams::Kube(params) => {
|
||||
@@ -217,9 +235,9 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update local file list
|
||||
pub(super) fn update_local_filelist(&mut self) {
|
||||
self.reload_local_dir();
|
||||
/// Update host bridge file list
|
||||
pub(super) fn update_host_bridge_filelist(&mut self) {
|
||||
self.reload_host_bridge_dir();
|
||||
// Get width
|
||||
let width = self
|
||||
.context_mut()
|
||||
@@ -228,29 +246,26 @@ impl FileTransferActivity {
|
||||
.size()
|
||||
.map(|x| (x.width / 2) - 2)
|
||||
.unwrap_or(0) as usize;
|
||||
let hostname: String = match hostname::get() {
|
||||
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 = self.get_hostbridge_hostname();
|
||||
|
||||
let hostname: String = format!(
|
||||
"{}:{} ",
|
||||
hostname,
|
||||
fmt_path_elide_ex(self.local().wrkdir.as_path(), width, hostname.len() + 3) // 3 because of '/…/'
|
||||
"{hostname}:{} ",
|
||||
fmt_path_elide_ex(
|
||||
self.host_bridge().wrkdir.as_path(),
|
||||
width,
|
||||
hostname.len() + 3
|
||||
) // 3 because of '/…/'
|
||||
);
|
||||
let files: Vec<Vec<TextSpan>> = self
|
||||
.local()
|
||||
.host_bridge()
|
||||
.iter_files()
|
||||
.map(|x| vec![TextSpan::from(self.local().fmt_file(x))])
|
||||
.map(|x| vec![TextSpan::from(self.host_bridge().fmt_file(x))])
|
||||
.collect();
|
||||
// Update content and title
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerLocal,
|
||||
&Id::ExplorerHostBridge,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
@@ -258,7 +273,7 @@ impl FileTransferActivity {
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerLocal,
|
||||
&Id::ExplorerHostBridge,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((hostname, Alignment::Left))
|
||||
)
|
||||
@@ -409,17 +424,19 @@ impl FileTransferActivity {
|
||||
self.browser.del_found();
|
||||
// Restore tab
|
||||
let new_tab = match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal => FileExplorerTab::Local,
|
||||
FileExplorerTab::FindHostBridge => FileExplorerTab::HostBridge,
|
||||
FileExplorerTab::FindRemote => FileExplorerTab::Remote,
|
||||
_ => FileExplorerTab::Local,
|
||||
_ => FileExplorerTab::HostBridge,
|
||||
};
|
||||
// Give focus to 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 => {
|
||||
assert!(self.app.active(&Id::ExplorerRemote).is_ok())
|
||||
}
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
assert!(self.app.active(&Id::ExplorerFind).is_ok())
|
||||
}
|
||||
}
|
||||
@@ -445,15 +462,21 @@ impl FileTransferActivity {
|
||||
|
||||
pub(super) fn update_browser_file_list(&mut self) {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn update_browser_file_list_swapped(&mut self) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_remote_filelist(),
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_local_filelist(),
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||
self.update_remote_filelist()
|
||||
}
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
|
||||
self.update_host_bridge_filelist()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,13 @@ use session::TransferPayload;
|
||||
use tempfile::TempDir;
|
||||
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::explorer::{FileExplorer, FileSorting};
|
||||
use crate::filetransfer::{Builder, FileTransferParams};
|
||||
use crate::host::Localhost;
|
||||
use crate::filetransfer::{
|
||||
FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder,
|
||||
};
|
||||
use crate::host::HostBridge;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::watcher::FsWatcher;
|
||||
|
||||
@@ -47,7 +49,7 @@ enum Id {
|
||||
ErrorPopup,
|
||||
ExecPopup,
|
||||
ExplorerFind,
|
||||
ExplorerLocal,
|
||||
ExplorerHostBridge,
|
||||
ExplorerRemote,
|
||||
FatalPopup,
|
||||
FileInfoPopup,
|
||||
@@ -68,7 +70,7 @@ enum Id {
|
||||
ReplacingFilesListPopup,
|
||||
SaveAsPopup,
|
||||
SortingPopup,
|
||||
StatusBarLocal,
|
||||
StatusBarHostBridge,
|
||||
StatusBarRemote,
|
||||
SymlinkPopup,
|
||||
SyncBrowsingMkdirPopup,
|
||||
@@ -213,8 +215,8 @@ pub struct FileTransferActivity {
|
||||
app: Application<Id, Msg, NoUserEvent>,
|
||||
/// Whether should redraw UI
|
||||
redraw: bool,
|
||||
/// Localhost bridge
|
||||
host: Localhost,
|
||||
/// Host bridge
|
||||
host_bridge: Box<dyn HostBridge>,
|
||||
/// Remote host client
|
||||
client: Box<dyn RemoteFs>,
|
||||
/// Browser
|
||||
@@ -229,26 +231,40 @@ pub struct FileTransferActivity {
|
||||
cache: Option<TempDir>,
|
||||
/// Fs watcher
|
||||
fswatcher: Option<FsWatcher>,
|
||||
/// connected once
|
||||
connected: bool,
|
||||
/// host bridge connected
|
||||
host_bridge_connected: bool,
|
||||
/// remote connected once
|
||||
remote_connected: bool,
|
||||
}
|
||||
|
||||
impl 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
|
||||
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 {
|
||||
exit_reason: None,
|
||||
context: None,
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.poll_timeout(ticks)
|
||||
.default_input_listener(ticks),
|
||||
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
||||
),
|
||||
redraw: true,
|
||||
host,
|
||||
client: Builder::build(params.protocol, params.params.clone(), &config_client),
|
||||
host_bridge,
|
||||
client: RemoteFsBuilder::build(
|
||||
remote_params.protocol,
|
||||
remote_params.params.clone(),
|
||||
&config_client,
|
||||
),
|
||||
browser: Browser::new(&config_client),
|
||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||
walkdir: WalkdirStates::default(),
|
||||
@@ -257,23 +273,22 @@ impl FileTransferActivity {
|
||||
Ok(d) => Some(d),
|
||||
Err(_) => None,
|
||||
},
|
||||
fswatcher: match FsWatcher::init(Duration::from_secs(5)) {
|
||||
Ok(w) => Some(w),
|
||||
Err(e) => {
|
||||
error!("failed to initialize fs watcher: {}", e);
|
||||
None
|
||||
}
|
||||
fswatcher: if enable_fs_watcher {
|
||||
FsWatcher::init(Duration::from_secs(5)).ok()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
connected: false,
|
||||
host_bridge_connected,
|
||||
remote_connected: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn local(&self) -> &FileExplorer {
|
||||
self.browser.local()
|
||||
fn host_bridge(&self) -> &FileExplorer {
|
||||
self.browser.host_bridge()
|
||||
}
|
||||
|
||||
fn local_mut(&mut self) -> &mut FileExplorer {
|
||||
self.browser.local_mut()
|
||||
fn host_bridge_mut(&mut self) -> &mut FileExplorer {
|
||||
self.browser.host_bridge_mut()
|
||||
}
|
||||
|
||||
fn remote(&self) -> &FileExplorer {
|
||||
@@ -361,7 +376,10 @@ impl Activity for FileTransferActivity {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// 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");
|
||||
// Configure text editor
|
||||
self.setup_text_editor();
|
||||
@@ -384,15 +402,34 @@ impl Activity for FileTransferActivity {
|
||||
if self.context.is_none() {
|
||||
return;
|
||||
}
|
||||
// Check if connected (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) {
|
||||
let ftparams = self.context().ft_params().unwrap();
|
||||
// Check if connected to host bridge (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||
if (!self.host_bridge.is_connected() || !self.host_bridge_connected)
|
||||
&& !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
|
||||
let msg: String = Self::get_connection_msg(&ftparams.params);
|
||||
// Set init state to connecting popup
|
||||
self.mount_blocking_wait(msg.as_str());
|
||||
// Connect to remote
|
||||
self.connect();
|
||||
self.connect_to_remote();
|
||||
// Redraw
|
||||
self.redraw = true;
|
||||
}
|
||||
@@ -432,6 +469,10 @@ impl Activity for FileTransferActivity {
|
||||
if self.client.is_connected() {
|
||||
let _ = self.client.disconnect();
|
||||
}
|
||||
// disconnect host bridge
|
||||
if self.host_bridge.is_connected() {
|
||||
let _ = self.host_bridge.disconnect();
|
||||
}
|
||||
self.context.take()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// Locals
|
||||
use std::fs::File as StdFile;
|
||||
use std::io::{Read, Seek, Write};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use remotefs::fs::{File, Metadata, ReadStream, UnixPex, Welcome, WriteStream};
|
||||
use remotefs::{RemoteError, RemoteErrorType, RemoteResult};
|
||||
@@ -26,10 +23,8 @@ const BUFSIZE: usize = 65535;
|
||||
enum TransferErrorReason {
|
||||
#[error("File transfer aborted")]
|
||||
Abrupted,
|
||||
#[error("Failed to seek file: {0}")]
|
||||
CouldNotRewind(std::io::Error),
|
||||
#[error("I/O error on localhost: {0}")]
|
||||
LocalIoError(std::io::Error),
|
||||
#[error("I/O error on host_bridgehost: {0}")]
|
||||
HostIoError(std::io::Error),
|
||||
#[error("Host error: {0}")]
|
||||
HostError(HostError),
|
||||
#[error("I/O error on remote: {0}")]
|
||||
@@ -50,15 +45,57 @@ pub(super) enum TransferPayload {
|
||||
}
|
||||
|
||||
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
|
||||
pub(super) fn connect(&mut self) {
|
||||
let ft_params = self.context().ft_params().unwrap().clone();
|
||||
pub(super) fn connect_to_remote(&mut self) {
|
||||
let ft_params = self.context().remote_params().unwrap().clone();
|
||||
let entry_dir: Option<PathBuf> = ft_params.remote_path;
|
||||
// Connect to remote
|
||||
match self.client.connect() {
|
||||
Ok(Welcome { banner, .. }) => {
|
||||
self.connected = self.client.is_connected();
|
||||
if !self.connected {
|
||||
self.remote_connected = self.client.is_connected();
|
||||
if !self.remote_connected {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,7 +131,7 @@ impl FileTransferActivity {
|
||||
self.umount_wait();
|
||||
self.reload_remote_dir();
|
||||
// Update file lists
|
||||
self.update_local_filelist();
|
||||
self.update_host_bridge_filelist();
|
||||
self.update_remote_filelist();
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -124,7 +161,7 @@ impl FileTransferActivity {
|
||||
|
||||
/// Reload remote directory entries and update browser
|
||||
pub(super) fn reload_remote_dir(&mut self) {
|
||||
if !self.connected {
|
||||
if !self.remote_connected {
|
||||
return;
|
||||
}
|
||||
// Get current entries
|
||||
@@ -149,35 +186,48 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reload local directory entries and update browser
|
||||
pub(super) fn reload_local_dir(&mut self) {
|
||||
self.mount_blocking_wait("Loading local directory...");
|
||||
/// Reload host_bridge directory entries and update browser
|
||||
pub(super) fn reload_host_bridge_dir(&mut self) {
|
||||
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();
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
self.local_mut().wrkdir = wrkdir;
|
||||
self.host_bridge_mut().wrkdir = wrkdir;
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not scan current local directory: {err}"),
|
||||
format!("Could not scan current host bridge directory: {err}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan current local directory
|
||||
fn local_scan(&mut self, path: &Path) -> Result<(), HostError> {
|
||||
match self.host.list_dir(path) {
|
||||
/// Scan current host bridge directory
|
||||
fn host_bridge_scan(&mut self, path: &Path) -> Result<(), HostError> {
|
||||
match self.host_bridge.list_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.local_mut().set_files(files);
|
||||
self.host_bridge_mut().set_files(files);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -270,7 +320,7 @@ impl FileTransferActivity {
|
||||
// Reset states
|
||||
self.transfer.reset();
|
||||
// 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);
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Uploading {}…", entry.path().display()));
|
||||
@@ -292,7 +342,7 @@ impl FileTransferActivity {
|
||||
// Calculate total size of transfer
|
||||
let total_transfer_size: usize = entries
|
||||
.iter()
|
||||
.map(|x| self.get_total_transfer_size_local(x))
|
||||
.map(|x| self.get_total_transfer_size_host(x))
|
||||
.sum();
|
||||
self.transfer.full.init(total_transfer_size);
|
||||
// Mount progress bar
|
||||
@@ -358,7 +408,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
// Get files in dir
|
||||
match self.host.list_dir(entry.path()) {
|
||||
match self.host_bridge.list_dir(entry.path()) {
|
||||
Ok(entries) => {
|
||||
// Iterate over files
|
||||
for entry in entries.iter() {
|
||||
@@ -433,17 +483,17 @@ impl FileTransferActivity {
|
||||
result
|
||||
}
|
||||
|
||||
/// Send local file and write it to remote path
|
||||
/// Send host_bridge file and write it to remote path
|
||||
fn filetransfer_send_one(
|
||||
&mut self,
|
||||
local: &File,
|
||||
host_bridge: &File,
|
||||
remote: &Path,
|
||||
file_name: String,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
// Sync file size and attributes before transfer
|
||||
let metadata = self
|
||||
.host
|
||||
.stat(local.path.as_path())
|
||||
.host_bridge
|
||||
.stat(host_bridge.path.as_path())
|
||||
.map_err(TransferErrorReason::HostError)
|
||||
.map(|x| x.metadata().clone())?;
|
||||
|
||||
@@ -452,22 +502,30 @@ impl FileTransferActivity {
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"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);
|
||||
return Ok(());
|
||||
}
|
||||
// Upload file
|
||||
// Try to open local file
|
||||
match self.host.open_file_read(local.path.as_path()) {
|
||||
Ok(fhnd) => match self.client.create(remote, &metadata) {
|
||||
Ok(rhnd) => {
|
||||
self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd)
|
||||
}
|
||||
Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => {
|
||||
self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd)
|
||||
}
|
||||
// Try to open host_bridge file
|
||||
match self.host_bridge.open_file(host_bridge.path.as_path()) {
|
||||
Ok(host_bridge_read) => match self.client.create(remote, &metadata) {
|
||||
Ok(rhnd) => self.filetransfer_send_one_with_stream(
|
||||
host_bridge,
|
||||
remote,
|
||||
file_name,
|
||||
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::HostError(err)),
|
||||
@@ -477,20 +535,21 @@ impl FileTransferActivity {
|
||||
/// Send file to remote using stream
|
||||
fn filetransfer_send_one_with_stream(
|
||||
&mut self,
|
||||
local: &File,
|
||||
host: &File,
|
||||
remote: &Path,
|
||||
file_name: String,
|
||||
mut reader: StdFile,
|
||||
mut reader: Box<dyn Read + Send>,
|
||||
mut writer: WriteStream,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
// 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
|
||||
self.transfer.partial.init(file_size);
|
||||
// rewind
|
||||
if let Err(err) = reader.rewind() {
|
||||
return Err(TransferErrorReason::CouldNotRewind(err));
|
||||
}
|
||||
|
||||
// Write remote file
|
||||
let mut total_bytes_written: usize = 0;
|
||||
let mut last_progress_val: f64 = 0.0;
|
||||
@@ -535,7 +594,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::LocalIoError(err));
|
||||
return Err(TransferErrorReason::HostIoError(err));
|
||||
}
|
||||
};
|
||||
// Increase progress
|
||||
@@ -561,14 +620,14 @@ impl FileTransferActivity {
|
||||
return Err(TransferErrorReason::Abrupted);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||
local.path.display(),
|
||||
host.path.display(),
|
||||
remote.display(),
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
@@ -580,30 +639,31 @@ impl FileTransferActivity {
|
||||
/// Send an `File` to remote without using streams.
|
||||
fn filetransfer_send_one_wno_stream(
|
||||
&mut self,
|
||||
local: &File,
|
||||
host: &File,
|
||||
remote: &Path,
|
||||
file_name: String,
|
||||
mut reader: StdFile,
|
||||
reader: Box<dyn Read + Send>,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
// Sync file size and attributes before transfer
|
||||
let metadata = self
|
||||
.host
|
||||
.stat(local.path.as_path())
|
||||
.host_bridge
|
||||
.stat(host.path.as_path())
|
||||
.map_err(TransferErrorReason::HostError)
|
||||
.map(|x| x.metadata().clone())?;
|
||||
// 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
|
||||
self.transfer.partial.init(file_size);
|
||||
// rewind
|
||||
if let Err(err) = reader.rewind() {
|
||||
return Err(TransferErrorReason::CouldNotRewind(err));
|
||||
}
|
||||
|
||||
// Draw before
|
||||
self.update_progress_bar(format!("Uploading \"{file_name}\"…"));
|
||||
self.view();
|
||||
// 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));
|
||||
}
|
||||
// set stat
|
||||
@@ -621,7 +681,7 @@ impl FileTransferActivity {
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||
local.path.display(),
|
||||
host.path.display(),
|
||||
remote.display(),
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
@@ -636,15 +696,17 @@ impl FileTransferActivity {
|
||||
pub(super) fn filetransfer_recv(
|
||||
&mut self,
|
||||
payload: TransferPayload,
|
||||
local_path: &Path,
|
||||
host_bridge_path: &Path,
|
||||
dst_name: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let result = match payload {
|
||||
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
|
||||
match &result {
|
||||
@@ -664,7 +726,7 @@ impl FileTransferActivity {
|
||||
fn filetransfer_recv_any(
|
||||
&mut self,
|
||||
entry: &File,
|
||||
local_path: &Path,
|
||||
host_path: &Path,
|
||||
dst_name: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
// Reset states
|
||||
@@ -675,14 +737,18 @@ impl FileTransferActivity {
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Downloading {}…", entry.path().display()));
|
||||
// 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
|
||||
self.umount_progress_bar();
|
||||
result
|
||||
}
|
||||
|
||||
/// 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
|
||||
self.transfer.reset();
|
||||
// Calculate total transfer size
|
||||
@@ -691,7 +757,7 @@ impl FileTransferActivity {
|
||||
// Mount progress bar
|
||||
self.mount_progress_bar(format!("Downloading {}…", entry.path.display()));
|
||||
// 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
|
||||
self.umount_progress_bar();
|
||||
// Return result
|
||||
@@ -728,7 +794,7 @@ impl FileTransferActivity {
|
||||
fn filetransfer_recv_recurse(
|
||||
&mut self,
|
||||
entry: &File,
|
||||
local_path: &Path,
|
||||
host_bridge_path: &Path,
|
||||
dst_name: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
// Write popup
|
||||
@@ -736,32 +802,35 @@ impl FileTransferActivity {
|
||||
// Match entry
|
||||
let result: Result<(), String> = if entry.is_dir() {
|
||||
// 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 {
|
||||
Some(name) => local_dir_path.push(name),
|
||||
None => local_dir_path.push(entry.name()),
|
||||
Some(name) => host_bridge_dir_path.push(name),
|
||||
None => host_bridge_dir_path.push(entry.name()),
|
||||
}
|
||||
// Create directory on local
|
||||
match self.host.mkdir_ex(local_dir_path.as_path(), true) {
|
||||
// Create directory on host_bridge
|
||||
match self
|
||||
.host_bridge
|
||||
.mkdir_ex(host_bridge_dir_path.as_path(), true)
|
||||
{
|
||||
Ok(_) => {
|
||||
// Apply file mode to directory
|
||||
if let Err(err) = self
|
||||
.host
|
||||
.setstat(local_dir_path.as_path(), entry.metadata())
|
||||
.host_bridge
|
||||
.setstat(host_bridge_dir_path.as_path(), entry.metadata())
|
||||
{
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not set stat to directory {:?} to \"{}\": {}",
|
||||
entry.metadata(),
|
||||
local_dir_path.display(),
|
||||
host_bridge_dir_path.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Created directory \"{}\"", local_dir_path.display()),
|
||||
format!("Created directory \"{}\"", host_bridge_dir_path.display()),
|
||||
);
|
||||
// Get files in dir
|
||||
match self.client.list_dir(entry.path()) {
|
||||
@@ -773,10 +842,10 @@ impl FileTransferActivity {
|
||||
break;
|
||||
}
|
||||
// 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(
|
||||
entry,
|
||||
local_dir_path.as_path(),
|
||||
host_bridge_dir_path.as_path(),
|
||||
None,
|
||||
)?
|
||||
}
|
||||
@@ -800,7 +869,7 @@ impl FileTransferActivity {
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Failed to create directory \"{}\": {}",
|
||||
local_dir_path.display(),
|
||||
host_bridge_dir_path.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
@@ -808,39 +877,39 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get local file
|
||||
let mut local_file_path: PathBuf = PathBuf::from(local_path);
|
||||
let local_file_name: String = match dst_name {
|
||||
// Get host_bridge file
|
||||
let mut host_bridge_file_path: PathBuf = PathBuf::from(host_bridge_path);
|
||||
let host_bridge_file_name: String = match dst_name {
|
||||
Some(n) => n,
|
||||
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
|
||||
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 matches!(
|
||||
err,
|
||||
TransferErrorReason::Abrupted | TransferErrorReason::LocalIoError(_)
|
||||
TransferErrorReason::Abrupted | TransferErrorReason::HostIoError(_)
|
||||
) {
|
||||
// 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(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not remove created file {}: {}",
|
||||
local_file_path.display(),
|
||||
host_bridge_file_path.display(),
|
||||
err
|
||||
),
|
||||
),
|
||||
Ok(entry) => {
|
||||
if let Err(err) = self.host.remove(&entry) {
|
||||
if let Err(err) = self.host_bridge.remove(&entry) {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not remove created file {}: {}",
|
||||
local_file_path.display(),
|
||||
host_bridge_file_path.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
@@ -853,8 +922,8 @@ impl FileTransferActivity {
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
// Reload directory on local
|
||||
self.reload_local_dir();
|
||||
// Reload directory on host_bridge
|
||||
self.reload_host_bridge_dir();
|
||||
// if aborted; show alert
|
||||
if self.transfer.aborted() {
|
||||
// Log abort
|
||||
@@ -866,15 +935,15 @@ impl FileTransferActivity {
|
||||
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(
|
||||
&mut self,
|
||||
local: &Path,
|
||||
host_bridge: &Path,
|
||||
remote: &File,
|
||||
file_name: String,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
// 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(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
@@ -888,16 +957,20 @@ impl FileTransferActivity {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try to open local file
|
||||
match self.host.open_file_write(local) {
|
||||
Ok(local_file) => {
|
||||
// Try to open host_bridge file
|
||||
match self.host_bridge.create_file(host_bridge, &remote.metadata) {
|
||||
Ok(writer) => {
|
||||
// Download file from remote
|
||||
match self.client.open(remote.path.as_path()) {
|
||||
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 => {
|
||||
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)),
|
||||
}
|
||||
@@ -909,16 +982,16 @@ impl FileTransferActivity {
|
||||
/// Receive an `File` from remote using stream
|
||||
fn filetransfer_recv_one_with_stream(
|
||||
&mut self,
|
||||
local: &Path,
|
||||
host_bridge: &Path,
|
||||
remote: &File,
|
||||
file_name: String,
|
||||
mut reader: ReadStream,
|
||||
mut writer: StdFile,
|
||||
mut writer: Box<dyn Write + Send>,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
let mut total_bytes_written: usize = 0;
|
||||
// Init transfer
|
||||
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_input_event_fetch: Option<Instant> = None;
|
||||
// While the entire file hasn't been completely read,
|
||||
@@ -951,7 +1024,7 @@ impl FileTransferActivity {
|
||||
match writer.write(&buffer[delta..bytes_read]) {
|
||||
Ok(bytes) => delta += bytes,
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::LocalIoError(err));
|
||||
return Err(TransferErrorReason::HostIoError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -984,14 +1057,20 @@ impl FileTransferActivity {
|
||||
if self.transfer.aborted() {
|
||||
return Err(TransferErrorReason::Abrupted);
|
||||
}
|
||||
|
||||
// finalize write
|
||||
self.host_bridge
|
||||
.finalize_write(writer)
|
||||
.map_err(TransferErrorReason::HostError)?;
|
||||
|
||||
// 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(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not set stat to file {:?} to \"{}\": {}",
|
||||
remote.metadata(),
|
||||
local.display(),
|
||||
host_bridge.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
@@ -1002,25 +1081,26 @@ impl FileTransferActivity {
|
||||
format!(
|
||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||
remote.path.display(),
|
||||
local.display(),
|
||||
host_bridge.display(),
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive an `File` from remote without using stream
|
||||
fn filetransfer_recv_one_wno_stream(
|
||||
&mut self,
|
||||
local: &Path,
|
||||
host_bridge: &Path,
|
||||
remote: &File,
|
||||
file_name: String,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
// Open local file
|
||||
// Open host_bridge file
|
||||
let reader = self
|
||||
.host
|
||||
.open_file_write(local)
|
||||
.host_bridge
|
||||
.create_file(host_bridge, &remote.metadata)
|
||||
.map_err(TransferErrorReason::HostError)
|
||||
.map(Box::new)?;
|
||||
// Init transfer
|
||||
@@ -1043,13 +1123,13 @@ impl FileTransferActivity {
|
||||
self.update_progress_bar(format!("Downloading \"{file_name}\""));
|
||||
self.view();
|
||||
// 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(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not set stat to file {:?} to \"{}\": {}",
|
||||
remote.metadata(),
|
||||
local.display(),
|
||||
host_bridge.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
@@ -1060,7 +1140,7 @@ impl FileTransferActivity {
|
||||
format!(
|
||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||
remote.path.display(),
|
||||
local.display(),
|
||||
host_bridge.display(),
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
),
|
||||
@@ -1068,20 +1148,47 @@ impl FileTransferActivity {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change directory for local
|
||||
pub(super) fn local_changedir(&mut self, path: &Path, push: bool) {
|
||||
/// Change directory for host_bridge
|
||||
pub(super) fn host_bridge_changedir(&mut self, path: &Path, push: bool) {
|
||||
// Get current directory
|
||||
let prev_dir: PathBuf = self.local().wrkdir.clone();
|
||||
let prev_dir: PathBuf = self.host_bridge().wrkdir.clone();
|
||||
// Change directory
|
||||
match self.host.change_wrkdir(path) {
|
||||
match self.host_bridge.change_wrkdir(path) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Changed directory on local: {}", path.display()),
|
||||
format!("Changed directory on host_bridge: {}", path.display()),
|
||||
);
|
||||
// Push prev_dir to stack
|
||||
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) => {
|
||||
@@ -1152,14 +1259,14 @@ impl FileTransferActivity {
|
||||
|
||||
// -- transfer sizes
|
||||
|
||||
/// Get total size of transfer for localhost
|
||||
fn get_total_transfer_size_local(&mut self, entry: &File) -> usize {
|
||||
/// Get total size of transfer for host_bridgehost
|
||||
fn get_total_transfer_size_host(&mut self, entry: &File) -> usize {
|
||||
if entry.is_dir() {
|
||||
// List dir
|
||||
match self.host.list_dir(entry.path()) {
|
||||
match self.host_bridge.list_dir(entry.path()) {
|
||||
Ok(files) => files
|
||||
.iter()
|
||||
.map(|x| self.get_total_transfer_size_local(x))
|
||||
.map(|x| self.get_total_transfer_size_host(x))
|
||||
.sum(),
|
||||
Err(err) => {
|
||||
self.log(
|
||||
@@ -1206,23 +1313,23 @@ impl FileTransferActivity {
|
||||
|
||||
// file changed
|
||||
|
||||
/// Check whether provided file has changed on local disk, compared to remote file
|
||||
fn has_local_file_changed(&self, local: &Path, remote: &File) -> bool {
|
||||
/// Check whether provided file has changed on host_bridge disk, compared to remote file
|
||||
fn has_host_bridge_file_changed(&mut self, host_bridge: &Path, remote: &File) -> bool {
|
||||
// check if files are equal (in case, don't transfer)
|
||||
if let Ok(local_file) = self.host.stat(local) {
|
||||
local_file.metadata().modified != remote.metadata().modified
|
||||
|| local_file.metadata().size != remote.metadata().size
|
||||
if let Ok(host_bridge_file) = self.host_bridge.stat(host_bridge) {
|
||||
host_bridge_file.metadata().modified != remote.metadata().modified
|
||||
|| host_bridge_file.metadata().size != remote.metadata().size
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether remote file has changed compared to local file
|
||||
fn has_remote_file_changed(&mut self, remote: &Path, local_metadata: &Metadata) -> bool {
|
||||
/// Checks whether remote file has changed compared to host_bridge file
|
||||
fn has_remote_file_changed(&mut self, remote: &Path, host_bridge_metadata: &Metadata) -> bool {
|
||||
// check if files are equal (in case, don't transfer)
|
||||
if let Ok(remote_file) = self.client.stat(remote) {
|
||||
local_metadata.modified != remote_file.metadata().modified
|
||||
|| local_metadata.size != remote_file.metadata().size
|
||||
host_bridge_metadata.modified != remote_file.metadata().modified
|
||||
|| host_bridge_metadata.size != remote_file.metadata().size
|
||||
} else {
|
||||
true
|
||||
}
|
||||
@@ -1230,11 +1337,11 @@ impl FileTransferActivity {
|
||||
|
||||
// -- file exist
|
||||
|
||||
pub(crate) fn local_file_exists(&mut self, p: &Path) -> bool {
|
||||
self.host.file_exists(p)
|
||||
pub(crate) fn host_bridge_file_exists(&mut self, p: &Path) -> bool {
|
||||
self.host_bridge.exists(p).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn remote_file_exists(&mut self, p: &Path) -> bool {
|
||||
self.client.stat(p).is_ok()
|
||||
self.client.exists(p).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,14 +40,12 @@ impl FileTransferActivity {
|
||||
self.umount_chmod();
|
||||
self.mount_blocking_wait("Applying new file mode…");
|
||||
match self.browser.tab() {
|
||||
#[cfg(unix)]
|
||||
FileExplorerTab::Local => self.action_local_chmod(mode),
|
||||
#[cfg(unix)]
|
||||
FileExplorerTab::FindLocal => self.action_find_local_chmod(mode),
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge
|
||||
if self.host_bridge.is_localhost() && cfg!(windows) => {}
|
||||
FileExplorerTab::HostBridge => self.action_local_chmod(mode),
|
||||
FileExplorerTab::FindHostBridge => self.action_find_local_chmod(mode),
|
||||
FileExplorerTab::Remote => self.action_remote_chmod(mode),
|
||||
FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode),
|
||||
#[cfg(windows)]
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => {}
|
||||
}
|
||||
self.umount_wait();
|
||||
self.update_browser_file_list();
|
||||
@@ -56,7 +54,7 @@ impl FileTransferActivity {
|
||||
self.umount_copy();
|
||||
self.mount_blocking_wait("Copying file(s)…");
|
||||
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),
|
||||
_ => panic!("Found tab doesn't support COPY"),
|
||||
}
|
||||
@@ -68,7 +66,7 @@ impl FileTransferActivity {
|
||||
self.umount_symlink();
|
||||
self.mount_blocking_wait("Creating symlink…");
|
||||
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),
|
||||
_ => panic!("Found tab doesn't support SYMLINK"),
|
||||
}
|
||||
@@ -80,9 +78,9 @@ impl FileTransferActivity {
|
||||
self.umount_radio_delete();
|
||||
self.mount_blocking_wait("Removing file(s)…");
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_delete(),
|
||||
FileExplorerTab::HostBridge => self.action_local_delete(),
|
||||
FileExplorerTab::Remote => self.action_remote_delete(),
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
// Get entry
|
||||
self.action_find_delete();
|
||||
// Delete entries
|
||||
@@ -108,20 +106,20 @@ impl FileTransferActivity {
|
||||
self.umount_wait();
|
||||
// Reload files
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.update_local_filelist(),
|
||||
FileExplorerTab::HostBridge => self.update_host_bridge_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(),
|
||||
}
|
||||
}
|
||||
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() {
|
||||
self.action_submit_local(entry);
|
||||
// Update file list if sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.update_remote_filelist();
|
||||
}
|
||||
self.update_local_filelist();
|
||||
self.update_host_bridge_filelist();
|
||||
}
|
||||
}
|
||||
TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => {
|
||||
@@ -129,7 +127,7 @@ impl FileTransferActivity {
|
||||
self.action_submit_remote(entry);
|
||||
// Update file list if sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.update_local_filelist();
|
||||
self.update_host_bridge_filelist();
|
||||
}
|
||||
self.update_remote_filelist();
|
||||
}
|
||||
@@ -150,7 +148,7 @@ impl FileTransferActivity {
|
||||
self.umount_exec();
|
||||
self.mount_blocking_wait(format!("Executing '{cmd}'…").as_str());
|
||||
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),
|
||||
_ => panic!("Found tab doesn't support EXEC"),
|
||||
}
|
||||
@@ -160,7 +158,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
TransferMsg::GoTo(dir) => {
|
||||
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),
|
||||
_ => panic!("Found tab doesn't support GOTO"),
|
||||
}
|
||||
@@ -175,18 +173,18 @@ impl FileTransferActivity {
|
||||
}
|
||||
TransferMsg::GoToParentDirectory => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => {
|
||||
FileExplorerTab::HostBridge => {
|
||||
self.action_go_to_local_upper_dir();
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.update_remote_filelist();
|
||||
}
|
||||
// Reload file list component
|
||||
self.update_local_filelist()
|
||||
self.update_host_bridge_filelist()
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
self.action_go_to_remote_upper_dir();
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.update_local_filelist();
|
||||
self.update_host_bridge_filelist();
|
||||
}
|
||||
// Reload file list component
|
||||
self.update_remote_filelist()
|
||||
@@ -196,18 +194,18 @@ impl FileTransferActivity {
|
||||
}
|
||||
TransferMsg::GoToPreviousDirectory => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => {
|
||||
FileExplorerTab::HostBridge => {
|
||||
self.action_go_to_previous_local_dir();
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.update_remote_filelist();
|
||||
}
|
||||
// Reload file list component
|
||||
self.update_local_filelist()
|
||||
self.update_host_bridge_filelist()
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
self.action_go_to_previous_remote_dir();
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.update_local_filelist();
|
||||
self.update_host_bridge_filelist();
|
||||
}
|
||||
// Reload file list component
|
||||
self.update_remote_filelist()
|
||||
@@ -220,7 +218,7 @@ impl FileTransferActivity {
|
||||
self.mount_walkdir_wait();
|
||||
// Find
|
||||
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(),
|
||||
_ => panic!("Trying to search for files, while already in a find result"),
|
||||
};
|
||||
@@ -242,13 +240,13 @@ impl FileTransferActivity {
|
||||
Ok(files) => {
|
||||
// Get wrkdir
|
||||
let wrkdir = match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.local().wrkdir.clone(),
|
||||
FileExplorerTab::HostBridge => self.host_bridge().wrkdir.clone(),
|
||||
_ => self.remote().wrkdir.clone(),
|
||||
};
|
||||
// Create explorer and load files
|
||||
self.browser.set_found(
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => FoundExplorerTab::Local,
|
||||
FileExplorerTab::HostBridge => FoundExplorerTab::Local,
|
||||
_ => FoundExplorerTab::Remote,
|
||||
},
|
||||
files,
|
||||
@@ -261,16 +259,16 @@ impl FileTransferActivity {
|
||||
self.update_find_list();
|
||||
// Initialize tab
|
||||
self.browser.change_tab(match self.browser.tab() {
|
||||
FileExplorerTab::Local => FileExplorerTab::FindLocal,
|
||||
FileExplorerTab::HostBridge => FileExplorerTab::FindHostBridge,
|
||||
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
|
||||
_ => FileExplorerTab::FindLocal,
|
||||
_ => FileExplorerTab::FindHostBridge,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
TransferMsg::Mkdir(dir) => {
|
||||
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),
|
||||
_ => {}
|
||||
}
|
||||
@@ -280,7 +278,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
TransferMsg::NewFile(name) => {
|
||||
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),
|
||||
_ => {}
|
||||
}
|
||||
@@ -289,15 +287,17 @@ impl FileTransferActivity {
|
||||
self.update_browser_file_list()
|
||||
}
|
||||
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::FindLocal | FileExplorerTab::FindRemote => self.action_find_open(),
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
self.action_find_open()
|
||||
}
|
||||
},
|
||||
TransferMsg::OpenFileWith(prog) => {
|
||||
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::FindLocal | FileExplorerTab::FindRemote => {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
self.action_find_open_with(&prog)
|
||||
}
|
||||
}
|
||||
@@ -305,7 +305,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
TransferMsg::OpenTextFile => {
|
||||
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(),
|
||||
_ => {}
|
||||
}
|
||||
@@ -316,7 +316,7 @@ impl FileTransferActivity {
|
||||
self.umount_rename();
|
||||
self.mount_blocking_wait("Moving file(s)…");
|
||||
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),
|
||||
_ => {}
|
||||
}
|
||||
@@ -336,9 +336,9 @@ impl FileTransferActivity {
|
||||
TransferMsg::SaveFileAs(dest) => {
|
||||
self.umount_saveas();
|
||||
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::FindLocal | FileExplorerTab::FindRemote => {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
// Get entry
|
||||
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::TransferFile => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_send(),
|
||||
FileExplorerTab::HostBridge => self.action_local_send(),
|
||||
FileExplorerTab::Remote => self.action_remote_recv(),
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
self.action_find_transfer(TransferOpts::default())
|
||||
}
|
||||
}
|
||||
@@ -371,8 +371,8 @@ impl FileTransferActivity {
|
||||
UiMsg::CloseChmodPopup => self.umount_chmod(),
|
||||
UiMsg::ChangeFileSorting(sorting) => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => {
|
||||
self.local_mut().sort_by(sorting);
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||
self.host_bridge_mut().sort_by(sorting);
|
||||
self.refresh_local_status_bar();
|
||||
}
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
|
||||
@@ -384,22 +384,28 @@ impl FileTransferActivity {
|
||||
}
|
||||
UiMsg::ChangeTransferWindow => {
|
||||
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::FindLocal | FileExplorerTab::Local => FileExplorerTab::Remote,
|
||||
FileExplorerTab::Remote if self.browser.found().is_some() => {
|
||||
FileExplorerTab::FindLocal
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
FileExplorerTab::Remote
|
||||
}
|
||||
FileExplorerTab::Remote if self.browser.found().is_some() => {
|
||||
FileExplorerTab::FindHostBridge
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
FileExplorerTab::HostBridge
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => FileExplorerTab::Local,
|
||||
};
|
||||
// Set focus
|
||||
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 => {
|
||||
assert!(self.app.active(&Id::ExplorerRemote).is_ok())
|
||||
}
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
assert!(self.app.active(&Id::ExplorerFind).is_ok())
|
||||
}
|
||||
}
|
||||
@@ -441,13 +447,13 @@ impl FileTransferActivity {
|
||||
let files = self.filter(&filter);
|
||||
// Get wrkdir
|
||||
let wrkdir = match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.local().wrkdir.clone(),
|
||||
FileExplorerTab::HostBridge => self.host_bridge().wrkdir.clone(),
|
||||
_ => self.remote().wrkdir.clone(),
|
||||
};
|
||||
// Create explorer and load files
|
||||
self.browser.set_found(
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => FoundExplorerTab::Local,
|
||||
FileExplorerTab::HostBridge => FoundExplorerTab::Local,
|
||||
_ => FoundExplorerTab::Remote,
|
||||
},
|
||||
files,
|
||||
@@ -458,9 +464,9 @@ impl FileTransferActivity {
|
||||
self.update_find_list();
|
||||
// Initialize tab
|
||||
self.browser.change_tab(match self.browser.tab() {
|
||||
FileExplorerTab::Local => FileExplorerTab::FindLocal,
|
||||
FileExplorerTab::HostBridge => FileExplorerTab::FindHostBridge,
|
||||
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
|
||||
_ => FileExplorerTab::FindLocal,
|
||||
_ => FileExplorerTab::FindHostBridge,
|
||||
});
|
||||
}
|
||||
UiMsg::FuzzySearch(needle) => {
|
||||
@@ -471,7 +477,7 @@ impl FileTransferActivity {
|
||||
assert!(self.app.active(&Id::Log).is_ok());
|
||||
}
|
||||
UiMsg::LogBackTabbed => {
|
||||
assert!(self.app.active(&Id::ExplorerLocal).is_ok());
|
||||
assert!(self.app.active(&Id::ExplorerHostBridge).is_ok());
|
||||
}
|
||||
UiMsg::Quit => {
|
||||
self.disconnect_and_quit();
|
||||
@@ -489,13 +495,15 @@ impl FileTransferActivity {
|
||||
UiMsg::ShowChmodPopup => {
|
||||
let selected_file = match self.browser.tab() {
|
||||
#[cfg(unix)]
|
||||
FileExplorerTab::Local => self.get_local_selected_entries(),
|
||||
FileExplorerTab::HostBridge => self.get_local_selected_entries(),
|
||||
#[cfg(unix)]
|
||||
FileExplorerTab::FindLocal => self.get_found_selected_entries(),
|
||||
FileExplorerTab::FindHostBridge => self.get_found_selected_entries(),
|
||||
FileExplorerTab::Remote => self.get_remote_selected_entries(),
|
||||
FileExplorerTab::FindRemote => self.get_found_selected_entries(),
|
||||
#[cfg(windows)]
|
||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => SelectedFile::None,
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||
SelectedFile::None
|
||||
}
|
||||
};
|
||||
if let Some(mode) = selected_file.unix_pex() {
|
||||
self.mount_chmod(
|
||||
@@ -516,7 +524,7 @@ impl FileTransferActivity {
|
||||
UiMsg::ShowDeletePopup => self.mount_radio_delete(),
|
||||
UiMsg::ShowDisconnectPopup => self.mount_disconnect(),
|
||||
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() {
|
||||
self.mount_file_info(&file);
|
||||
}
|
||||
@@ -543,9 +551,9 @@ impl FileTransferActivity {
|
||||
UiMsg::ShowSaveAsPopup => self.mount_saveas(),
|
||||
UiMsg::ShowSymlinkPopup => {
|
||||
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::FindLocal | FileExplorerTab::FindRemote => false,
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => false,
|
||||
} {
|
||||
// Only if only one entry is selected
|
||||
self.mount_symlink();
|
||||
@@ -558,8 +566,8 @@ impl FileTransferActivity {
|
||||
UiMsg::ShowWatchedPathsList => self.action_show_watched_paths_list(),
|
||||
UiMsg::ShowWatcherPopup => self.action_show_radio_watch(),
|
||||
UiMsg::ToggleHiddenFiles => match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||
self.browser.local_mut().toggle_hidden_files();
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.browser.host_bridge_mut().toggle_hidden_files();
|
||||
self.refresh_local_status_bar();
|
||||
self.update_browser_file_list();
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
use remotefs::fs::{File, UnixPex};
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{PropPayload, PropValue, TextSpan};
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::tui::widgets::Clear;
|
||||
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::ratatui::widgets::Clear;
|
||||
use tuirealm::{AttrValue, Attribute, Sub, SubClause, SubEventClause};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -44,7 +44,7 @@ impl FileTransferActivity {
|
||||
assert!(self
|
||||
.app
|
||||
.mount(
|
||||
Id::ExplorerLocal,
|
||||
Id::ExplorerHostBridge,
|
||||
Box::new(components::ExplorerLocal::new(
|
||||
"",
|
||||
&[],
|
||||
@@ -81,12 +81,12 @@ impl FileTransferActivity {
|
||||
self.refresh_local_status_bar();
|
||||
self.refresh_remote_status_bar();
|
||||
// Update components
|
||||
self.update_local_filelist();
|
||||
self.update_host_bridge_filelist();
|
||||
// self.update_remote_filelist();
|
||||
// Global listener
|
||||
self.mount_global_listener();
|
||||
// Give focus to local explorer
|
||||
assert!(self.app.active(&Id::ExplorerLocal).is_ok());
|
||||
assert!(self.app.active(&Id::ExplorerHostBridge).is_ok());
|
||||
}
|
||||
|
||||
// -- view
|
||||
@@ -106,7 +106,7 @@ impl FileTransferActivity {
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
.split(f.area());
|
||||
// main chunks
|
||||
let main_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -141,7 +141,7 @@ impl FileTransferActivity {
|
||||
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) {
|
||||
self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]);
|
||||
} else {
|
||||
self.app.view(&Id::ExplorerLocal, f, tabs_chunks[0]);
|
||||
self.app.view(&Id::ExplorerHostBridge, f, tabs_chunks[0]);
|
||||
}
|
||||
// @! Remote explorer (Find or default)
|
||||
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) {
|
||||
@@ -152,80 +152,81 @@ impl FileTransferActivity {
|
||||
// Draw log box
|
||||
self.app.view(&Id::Log, f, bottom_chunks[1]);
|
||||
// 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]);
|
||||
// @! Draw popups
|
||||
if self.app.mounted(&Id::FatalPopup) {
|
||||
let popup = Popup(
|
||||
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);
|
||||
// make popup
|
||||
self.app.view(&Id::FatalPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::CopyPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::ChmodPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::FilterPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::GotoPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::MkdirPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::NewfilePopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::OpenWithPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::RenamePopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::SaveAsPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::SymlinkPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::ExecPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::FileInfoPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
let popup_chunks = Layout::default()
|
||||
@@ -241,14 +242,14 @@ impl FileTransferActivity {
|
||||
self.app.view(&Id::ProgressBarFull, f, popup_chunks[0]);
|
||||
self.app.view(&Id::ProgressBarPartial, f, popup_chunks[1]);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::DeletePopup, f, popup);
|
||||
} else if self.app.mounted(&Id::ReplacePopup) {
|
||||
// NOTE: handle extended / normal modes
|
||||
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);
|
||||
let popup_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -264,42 +265,42 @@ impl FileTransferActivity {
|
||||
.view(&Id::ReplacingFilesListPopup, f, popup_chunks[0]);
|
||||
self.app.view(&Id::ReplacePopup, f, popup_chunks[1]);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::ReplacePopup, f, popup);
|
||||
}
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::DisconnectPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::QuitPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::WatchedPathsList, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::WatcherPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::SortingPopup, f, popup);
|
||||
} else if self.app.mounted(&Id::ErrorPopup) {
|
||||
let popup = Popup(
|
||||
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);
|
||||
// make popup
|
||||
self.app.view(&Id::ErrorPopup, f, popup);
|
||||
@@ -312,17 +313,17 @@ impl FileTransferActivity {
|
||||
.unwrap_or(1) as u16;
|
||||
|
||||
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);
|
||||
// make popup
|
||||
self.app.view(&Id::WaitPopup, f, popup);
|
||||
} 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);
|
||||
// make popup
|
||||
self.app.view(&Id::SyncBrowsingMkdirPopup, f, popup);
|
||||
} 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);
|
||||
// make 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) {
|
||||
// Get color
|
||||
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_foreground,
|
||||
self.theme().transfer_local_explorer_highlighted,
|
||||
@@ -763,7 +764,7 @@ impl FileTransferActivity {
|
||||
pub(super) fn mount_file_sorting(&mut self) {
|
||||
let sorting_color = self.theme().transfer_status_sorting;
|
||||
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(),
|
||||
_ => return,
|
||||
};
|
||||
@@ -901,7 +902,7 @@ impl FileTransferActivity {
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::StatusBarLocal,
|
||||
Id::StatusBarHostBridge,
|
||||
Box::new(components::StatusBarLocal::new(
|
||||
&self.browser,
|
||||
sorting_color,
|
||||
@@ -1066,138 +1067,34 @@ impl FileTransferActivity {
|
||||
|
||||
/// Returns a sub clause which requires that no popup is mounted in order to be satisfied
|
||||
fn no_popup_mounted_clause() -> SubClause<Id> {
|
||||
SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::CopyPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::DeletePopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::DisconnectPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::ErrorPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::ExecPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::FatalPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::FileInfoPopup,
|
||||
)))),
|
||||
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,
|
||||
)))),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
tuirealm::subclause_and_not!(
|
||||
Id::CopyPopup,
|
||||
Id::DeletePopup,
|
||||
Id::DisconnectPopup,
|
||||
Id::ErrorPopup,
|
||||
Id::ExecPopup,
|
||||
Id::FatalPopup,
|
||||
Id::FileInfoPopup,
|
||||
Id::GotoPopup,
|
||||
Id::KeybindingsPopup,
|
||||
Id::MkdirPopup,
|
||||
Id::NewfilePopup,
|
||||
Id::OpenWithPopup,
|
||||
Id::ProgressBarFull,
|
||||
Id::ProgressBarPartial,
|
||||
Id::ExplorerFind,
|
||||
Id::QuitPopup,
|
||||
Id::RenamePopup,
|
||||
Id::ReplacePopup,
|
||||
Id::SaveAsPopup,
|
||||
Id::SortingPopup,
|
||||
Id::SyncBrowsingMkdirPopup,
|
||||
Id::SymlinkPopup,
|
||||
Id::WatcherPopup,
|
||||
Id::WatchedPathsList,
|
||||
Id::ChmodPopup,
|
||||
Id::WaitPopup,
|
||||
Id::FilterPopup
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ pub mod auth;
|
||||
pub mod filetransfer;
|
||||
pub mod setup;
|
||||
|
||||
const CROSSTERM_MAX_POLL: usize = 10;
|
||||
|
||||
// -- Exit reason
|
||||
|
||||
pub enum ExitReason {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Locals
|
||||
use std::env;
|
||||
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
use tuirealm::{State, StateValue};
|
||||
|
||||
use super::{Id, IdSsh, IdTheme, SetupActivity, ViewLayout};
|
||||
|
||||
@@ -19,7 +19,7 @@ use tuirealm::listener::EventListenerCfg;
|
||||
use tuirealm::props::Color;
|
||||
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::system::config_client::ConfigClient;
|
||||
use crate::system::theme_provider::ThemeProvider;
|
||||
@@ -262,7 +262,7 @@ impl SetupActivity {
|
||||
Self {
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.default_input_listener(ticks)
|
||||
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL)
|
||||
.poll_timeout(ticks),
|
||||
),
|
||||
exit_reason: None,
|
||||
|
||||
@@ -8,7 +8,7 @@ pub mod ssh_keys;
|
||||
pub mod theme;
|
||||
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::tui::widgets::Clear;
|
||||
use tuirealm::ratatui::widgets::Clear;
|
||||
use tuirealm::{Frame, Sub, SubClause, SubEventClause};
|
||||
|
||||
use super::*;
|
||||
@@ -112,23 +112,23 @@ impl SetupActivity {
|
||||
|
||||
pub(super) fn view_popups(&mut self, f: &mut Frame) {
|
||||
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);
|
||||
// make popup
|
||||
self.app.view(&Id::Common(IdCommon::ErrorPopup), f, popup);
|
||||
} else if self.app.mounted(&Id::Common(IdCommon::QuitPopup)) {
|
||||
// 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);
|
||||
self.app.view(&Id::Common(IdCommon::QuitPopup), f, popup);
|
||||
} else if self.app.mounted(&Id::Common(IdCommon::Keybindings)) {
|
||||
// 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);
|
||||
self.app.view(&Id::Common(IdCommon::Keybindings), f, popup);
|
||||
} else if self.app.mounted(&Id::Common(IdCommon::SavePopup)) {
|
||||
// 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);
|
||||
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
|
||||
fn no_popup_mounted_clause() -> SubClause<Id> {
|
||||
SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
|
||||
IdCommon::ErrorPopup,
|
||||
))))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(Id::Common(
|
||||
IdCommon::Keybindings,
|
||||
))))),
|
||||
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,
|
||||
))))),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
tuirealm::subclause_and_not!(
|
||||
Id::Common(IdCommon::ErrorPopup),
|
||||
Id::Common(IdCommon::Keybindings),
|
||||
Id::Common(IdCommon::QuitPopup),
|
||||
Id::Common(IdCommon::SavePopup),
|
||||
Id::Ssh(IdSsh::DelSshKeyPopup),
|
||||
Id::Ssh(IdSsh::SshHost)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::{State, StateValue};
|
||||
|
||||
use super::{
|
||||
@@ -50,7 +50,7 @@ impl SetupActivity {
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
.split(f.area());
|
||||
// Render common widget
|
||||
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
|
||||
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
// Locals
|
||||
// Ext
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::tui::widgets::Clear;
|
||||
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::ratatui::widgets::Clear;
|
||||
|
||||
use super::{components, Context, Id, IdCommon, IdSsh, SetupActivity, ViewLayout};
|
||||
use crate::utils::ui::{Popup, Size};
|
||||
@@ -37,7 +37,7 @@ impl SetupActivity {
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
.split(f.area());
|
||||
// Render common widget
|
||||
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
|
||||
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
|
||||
@@ -45,11 +45,11 @@ impl SetupActivity {
|
||||
// Popups
|
||||
self.view_popups(f);
|
||||
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);
|
||||
self.app.view(&Id::Ssh(IdSsh::DelSshKeyPopup), f, popup);
|
||||
} 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);
|
||||
let popup_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
// Locals
|
||||
// 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};
|
||||
|
||||
@@ -39,7 +39,7 @@ impl SetupActivity {
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
.split(f.area());
|
||||
// Render common widget
|
||||
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
|
||||
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
|
||||
|
||||
@@ -3,21 +3,22 @@
|
||||
//! `Context` is the module which provides all the functionalities related to the UI data holder, called Context
|
||||
|
||||
// Locals
|
||||
use tuirealm::terminal::TerminalBridge;
|
||||
use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalBridge};
|
||||
|
||||
use super::store::Store;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::filetransfer::{FileTransferParams, HostBridgeParams};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::theme_provider::ThemeProvider;
|
||||
|
||||
/// Context holds data structures shared by the activities
|
||||
pub struct Context {
|
||||
ft_params: Option<FileTransferParams>,
|
||||
host_bridge_params: Option<HostBridgeParams>,
|
||||
remote_params: Option<FileTransferParams>,
|
||||
bookmarks_client: Option<BookmarksClient>,
|
||||
config_client: ConfigClient,
|
||||
pub(crate) store: Store,
|
||||
pub(crate) terminal: TerminalBridge,
|
||||
pub(crate) terminal: TerminalBridge<CrosstermTerminalAdapter>,
|
||||
theme_provider: ThemeProvider,
|
||||
error: Option<String>,
|
||||
}
|
||||
@@ -30,27 +31,29 @@ impl Context {
|
||||
theme_provider: ThemeProvider,
|
||||
error: Option<String>,
|
||||
) -> Context {
|
||||
let mut ctx = Context {
|
||||
let mut terminal = TerminalBridge::init_crossterm().expect("Could not initialize terminal");
|
||||
let _ = terminal.disable_mouse_capture();
|
||||
|
||||
Context {
|
||||
bookmarks_client,
|
||||
config_client,
|
||||
ft_params: None,
|
||||
host_bridge_params: None,
|
||||
remote_params: None,
|
||||
store: Store::init(),
|
||||
terminal: TerminalBridge::new().expect("Could not initialize terminal"),
|
||||
terminal,
|
||||
theme_provider,
|
||||
error,
|
||||
};
|
||||
|
||||
// Init terminal state
|
||||
let _ = ctx.terminal.enable_raw_mode();
|
||||
let _ = ctx.terminal.enter_alternate_screen();
|
||||
|
||||
ctx
|
||||
}
|
||||
}
|
||||
|
||||
// -- getters
|
||||
|
||||
pub fn ft_params(&self) -> Option<&FileTransferParams> {
|
||||
self.ft_params.as_ref()
|
||||
pub fn remote_params(&self) -> Option<&FileTransferParams> {
|
||||
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> {
|
||||
@@ -85,23 +88,22 @@ impl Context {
|
||||
&mut self.theme_provider
|
||||
}
|
||||
|
||||
pub fn terminal(&mut self) -> &mut TerminalBridge {
|
||||
pub fn terminal(&mut self) -> &mut TerminalBridge<CrosstermTerminalAdapter> {
|
||||
&mut self.terminal
|
||||
}
|
||||
|
||||
// -- setter
|
||||
|
||||
pub fn set_ftparams(&mut self, params: FileTransferParams) {
|
||||
self.ft_params = Some(params);
|
||||
pub fn set_remote_params(&mut self, params: FileTransferParams) {
|
||||
self.remote_params = Some(params);
|
||||
}
|
||||
|
||||
pub fn set_host_bridge_params(&mut self, params: HostBridgeParams) {
|
||||
self.host_bridge_params = Some(params);
|
||||
}
|
||||
|
||||
// -- 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
|
||||
pub fn error(&mut self) -> Option<String> {
|
||||
self.error.take()
|
||||
@@ -110,8 +112,8 @@ impl Context {
|
||||
|
||||
impl Drop for Context {
|
||||
fn drop(&mut self) {
|
||||
// Re-enable terminal stuff
|
||||
let _ = self.terminal.disable_raw_mode();
|
||||
let _ = self.terminal.leave_alternate_screen();
|
||||
if let Err(err) = self.terminal.restore() {
|
||||
error!("Could not restore terminal: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::time::{Duration, SystemTime};
|
||||
|
||||
use chrono::prelude::*;
|
||||
use remotefs::fs::UnixPexClass;
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Convert permissions bytes of permissions value into ls notation (e.g. rwx,-wx,--x)
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::str::FromStr;
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use lazy_regex::{Lazy, Regex};
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
use tuirealm::utils::parser as tuirealm_parser;
|
||||
|
||||
#[cfg(smb)]
|
||||
@@ -102,7 +102,7 @@ static REMOTE_SMB_OPT_REGEX: Lazy<Regex> =
|
||||
* - group 1: Version
|
||||
* E.g. termscp-0.3.2 => 0.3.2; v0.4.0 => 0.4.0
|
||||
*/
|
||||
static SEMVER_REGEX: Lazy<Regex> = lazy_regex!(r".*(:?[0-9]\.[0-9]\.[0-9])");
|
||||
static SEMVER_REGEX: Lazy<Regex> = lazy_regex!(r"v?((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*))");
|
||||
|
||||
/**
|
||||
* Regex matches:
|
||||
@@ -835,6 +835,8 @@ mod tests {
|
||||
assert_eq!(parse_semver("v0.4.1").unwrap(), String::from("0.4.1"),);
|
||||
assert_eq!(parse_semver("1.0.0").unwrap(), String::from("1.0.0"),);
|
||||
assert!(parse_semver("v1.1").is_none());
|
||||
|
||||
assert_eq!(parse_semver("10.15.10"), Some("10.15.10".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,11 +2,26 @@
|
||||
//!
|
||||
//! `Utils` implements utilities functions to work with layouts
|
||||
|
||||
use tuirealm::terminal::{TerminalAdapter, TerminalBridge};
|
||||
|
||||
/// Read a secret from tty with customisable prompt
|
||||
pub fn read_secret_from_tty(prompt: &str) -> std::io::Result<Option<String>> {
|
||||
match rpassword::prompt_password(prompt) {
|
||||
pub fn read_secret_from_tty<T>(
|
||||
terminal_bridge: &mut TerminalBridge<T>,
|
||||
prompt: impl ToString,
|
||||
) -> std::io::Result<Option<String>>
|
||||
where
|
||||
T: TerminalAdapter,
|
||||
{
|
||||
let _ = terminal_bridge.disable_raw_mode();
|
||||
let _ = terminal_bridge.leave_alternate_screen();
|
||||
let res = match rpassword::prompt_password(prompt) {
|
||||
Ok(p) if p.is_empty() => Ok(None),
|
||||
Ok(p) => Ok(Some(p)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
};
|
||||
|
||||
let _ = terminal_bridge.enter_alternate_screen();
|
||||
let _ = terminal_bridge.enable_raw_mode();
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! `Utils` implements utilities functions to work with layouts
|
||||
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tuirealm::ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
|
||||
/// Size type for UI renders
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
|
||||
Reference in New Issue
Block a user