19 Commits

Author SHA1 Message Date
veeso
5f7a0d8a46 fix: install.sh deb name
Some checks failed
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-12-02 14:27:34 +01:00
veeso
694232564a fix: install.sh deb name 2025-12-02 14:25:55 +01:00
veeso
54b674ad43 ci: windows artifact name
Some checks failed
Deploy static content to Pages / deploy (push) Has been cancelled
Windows / build (push) Has been cancelled
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-11-11 12:34:36 +01:00
veeso
c32822037e ci: deploy site 2025-11-11 12:21:05 +01:00
veeso
abb5c212c5 feat: Merge branch '0.19.0' 2025-11-11 12:19:21 +01:00
veeso
e9b54a227b chore: CHANGELOG date 2025-11-11 09:42:21 +01:00
veeso
befc32198a ci: debian fix
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
2025-11-10 17:25:29 +01:00
veeso
7e5103ff7e ci: Debian 2025-11-10 17:06:44 +01:00
veeso
2cb600083e docs: Release date 2025-11-10 16:44:24 +01:00
Christian Visintin
47d23673e6 ci: Build artifacts for Windows x86_64 and Ubuntu x86_64 (#368) 2025-11-10 16:43:25 +01:00
Christian Visintin
a0b357cf8c feat: Added <CTRL+S> keybinding to get the total size of selected paths. (#367)
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
* feat: Added `<CTRL+S>` keybinding to get the total size of selected paths.

closes #297
2025-11-09 21:14:42 +01:00
Christian Visintin
75943f2b93 feat: Changed file overwrite behaviour (#366)
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Now the user can choose for each file whether to overwrite, skip or overwrite all/skip all.

closes #335
2025-11-09 19:00:17 +01:00
veeso
085ab721f9 build: remotefs-ssh 0.7.1
This version fixes compatibility with hosts which don't use bash/sh as the default shell.

closes #365
2025-11-09 17:38:50 +01:00
Christian Visintin
f4156a5059 feat: Import bookmarks from ssh config with a CLI command (#364)
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
* feat: Import bookmarks from ssh config with a CLI command

Use import-ssh-hosts to import all the possible hosts by the configured ssh config or the default one on your machine

closes #331
2025-11-08 15:32:52 +01:00
Christian Visintin
4bebec369f fix: Issues with update checks (#363)
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Removed error popup message if failed to check for updates.
Prevent long timeouts when checking for updates if the network is down or the DNS is not working.

closes #354
2025-10-02 21:27:51 +02:00
Christian Visintin
05c8613279 fix: Report a message while calculating total size of files to transfer. (#362)
* fix: Report a message while calculating total size of files to transfer.

Currently, in case of huge transfers the app may look frozen while calculating the transfer size. We should at least report to the user we are actually doing something.

closes #361

* ci: windows runner
2025-10-02 20:58:26 +02:00
veeso
205d2813ad perf: Migrated to libssh.org on Linux and MacOS for better ssh agent support.
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
closes #337
2025-09-20 18:13:20 +02:00
veeso
86660a0cc9 fix: SMB support for MacOS with vendored build of libsmbclient.
closes #334
2025-09-20 18:07:26 +02:00
veeso
3c79e812eb build: 0.19 deps 2025-09-06 16:40:01 +02:00
59 changed files with 1839 additions and 1134 deletions

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch:
env:
TERMSCP_VERSION: "0.18.0"
TERMSCP_VERSION: "0.19.0"
jobs:
build-binaries:
@@ -14,13 +14,24 @@ jobs:
platform:
- release_for: MacOS-x86_64
os: macos-latest
platform: macos
target: x86_64-apple-darwin
script: macos.sh
- release_for: MacOS-M1
- release_for: MacOS-aarch64
os: macos-latest
platform: macos
target: aarch64-apple-darwin
script: macos.sh
- release_for: Linux-x86_64
os: ubuntu-latest
platform: linux
target: x86_64-unknown-linux-gnu
debian_suffix: amd64
- release_for: Windows-x86_64
os: windows-latest
platform: windows
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform.os }}
steps:
@@ -29,18 +40,122 @@ jobs:
with:
toolchain: stable
targets: ${{ matrix.platform.target }}
- name: Build release
run: cargo build --release --target ${{ matrix.platform.target }}
- name: Prepare artifact files
- name: Install dependencies (Linux)
if: matrix.platform.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y \
make \
libgit2-dev \
build-essential \
pkg-config \
libbsd-dev \
libcap-dev \
libcups2-dev \
libgnutls28-dev \
libicu-dev \
libjansson-dev \
libkeyutils-dev \
libldap2-dev \
zlib1g-dev \
libpam0g-dev \
libacl1-dev \
libarchive-dev \
flex \
bison \
libntirpc-dev \
libtracker-sparql-3.0-dev \
libglib2.0-dev \
libdbus-1-dev \
libsasl2-dev \
libunistring-dev \
libdbus-1-dev \
cpanminus;
sudo cpanm Parse::Yapp::Driver
- name: Install dependencies (MacOS)
if: matrix.platform.platform == 'macos'
run: |
brew update
brew install \
bison \
cpanminus \
cups \
flex \
gettext \
gmp \
gnutls \
icu4c \
jansson \
libarchive \
libbsd \
libunistring \
libgit2 \
libtirpc \
openldap \
pkg-config \
zlib
brew link --force bison
brew link --force cups
brew link --force flex
brew link --force gettext
brew link --force gmp
brew link --force gnutls
brew link --force icu4c
brew link --force jansson
brew link --force libarchive
brew link --force libbsd
brew link --force libgit2
brew link --force libtirpc
brew link --force libunistring
brew link --force openldap
brew link --force zlib
cpanm Parse::Yapp::Driver
- name: Build release (MacOS Intel)
if: matrix.platform.target == 'x86_64-apple-darwin'
run: cargo build --release --no-default-features --features keyring --target ${{ matrix.platform.target }}
- name: Build release (others)
if: matrix.platform.target != 'x86_64-apple-darwin'
run: cargo build --release --features smb-vendored --target ${{ matrix.platform.target }}
- name: Build deb
if: matrix.platform.platform == 'linux'
run: |
cargo install cargo-deb
cargo deb --target ${{ matrix.platform.target }} --features smb-vendored
- name: Prepare artifact files (Posix)
if: matrix.platform.platform != 'windows'
run: |
mkdir -p .artifact
mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp
tar -czf .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz -C .artifact termscp
ls -l .artifact/
- name: "Upload artifact"
- name: Upload artifact (Posix)
if: matrix.platform.platform != 'windows'
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
retention-days: 1
name: termscp-${{ matrix.platform.target }}
path: .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz
- name: Upload artifact (Windows)
if: matrix.platform.platform == 'windows'
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
retention-days: 1
name: termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}
path: target/${{ matrix.platform.target }}/release/termscp.exe
- name: Upload artifact (Deb)
if: matrix.platform.platform == 'linux'
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
retention-days: 1
name: termscp-${{ matrix.platform.target }}-deb
path: target/debian/termscp_${{ env.TERMSCP_VERSION }}-1_${{ matrix.platform.debian_suffix }}.deb

View File

@@ -22,6 +22,13 @@ jobs:
with:
toolchain: stable
components: rustfmt, clippy
- name: Install dependencies
run: |
brew update
brew install \
pkg-config \
samba
brew link --force samba
- name: Build
run: cargo build
- name: Run tests

View File

@@ -6,7 +6,8 @@ on:
push:
branches: ["main"]
paths:
- "./site/**/*"
- ".github/workflows/website.yml"
- "site/**"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

View File

@@ -15,7 +15,7 @@ env:
jobs:
build:
runs-on: windows-2019
runs-on: windows-latest
steps:
- uses: actions/checkout@v2

View File

@@ -1,6 +1,7 @@
# Changelog
- [Changelog](#changelog)
- [0.19.0](#0190)
- [0.18.0](#0180)
- [0.17.0](#0170)
- [0.16.1](#0161)
@@ -41,9 +42,25 @@
---
## 0.19.0
Released on 11/11/2025
- [Issue 297](https://github.com/veeso/termscp/issues/297): Added `<CTRL+S>` keybinding to get the total size of selected paths.
- [Issue 331](https://github.com/veeso/termscp/issues/331): Added new `import-ssh-hosts` CLI subcommand to import all the hosts from the ssh config as bookmarks.
- [Issue 335](https://github.com/veeso/termscp/issues/335): Changed file overwrite behaviour
- Now the user can choose for each file whether to overwrite, skip or overwrite all/skip all.
- [Issue 354](https://github.com/veeso/termscp/issues/354):
- Removed error popup message if failed to check for updates.
- Prevent long timeouts when checking for updates if the network is down or the DNS is not working.
- [Issue 356](https://github.com/veeso/termscp/issues/356): Fixed SSH auth issue not trying with the password if any RSA key was found.
- [Issue 334](https://github.com/veeso/termscp/issues/334): SMB support for MacOS with vendored build of libsmbclient.
- [Issue 337](https://github.com/veeso/termscp/issues/337): Migrated to libssh.org on Linux and MacOS for better ssh agent support.
- [Issue 361](https://github.com/veeso/termscp/issues/361): Report a message while calculating total size of files to transfer.
## 0.18.0
Released on 10/06/2025
Released on 11/11/2025
- 🐚 An **Embedded shell for termscp**:
- [Issue 340](https://github.com/veeso/termscp/issues/340): Replaced the `Exec` popup with a **fully functional terminal emulator** embedded thanks to [A-Kenji's tui-term](https://github.com/a-kenji/tui-term).

1221
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,8 @@ license = "MIT"
name = "termscp"
readme = "README.md"
repository = "https://github.com/veeso/termscp"
version = "0.18.0"
rust-version = "1.85.1"
version = "0.19.0"
rust-version = "1.88.0"
[package.metadata.rpm]
package = "termscp"
@@ -60,6 +60,7 @@ regex = "^1"
remotefs = "^0.3"
remotefs-aws-s3 = "0.4"
remotefs-kube = "0.4"
remotefs-smb = { version = "^0.3", optional = true }
remotefs-webdav = "^0.2"
rpassword = "^7"
self_update = { version = "^0.42", default-features = false, features = [
@@ -70,12 +71,13 @@ self_update = { version = "^0.42", default-features = false, features = [
"compression-zip-deflate",
] }
serde = { version = "^1", features = ["derive"] }
shellexpand = "3"
simplelog = "^0.12"
ssh2-config = "^0.5"
ssh2-config = "^0.6"
tempfile = "3"
thiserror = "2"
tokio = { version = "1.44", features = ["rt"] }
toml = "^0.8"
tokio = { version = "1", features = ["rt"] }
toml = "^0.9"
tui-realm-stdlib = "3"
tuirealm = "3"
tui-term = "0.2"
@@ -84,17 +86,20 @@ version-compare = "^0.2"
whoami = "^1.6"
wildmatch = "^2"
[target."cfg(not(target_os = \"macos\"))".dependencies]
remotefs-smb = { version = "^0.3", optional = true }
[target."cfg(target_family = \"unix\")".dependencies]
remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
remotefs-ssh = { version = "^0.6", features = ["ssh2-vendored"] }
remotefs-ftp = { version = "^0.3", features = [
"native-tls-vendored",
"native-tls",
] }
remotefs-ssh = { version = "^0.7", default-features = false, features = [
"find",
"libssh-vendored",
] }
uzers = "0.12"
[target."cfg(target_family = \"windows\")".dependencies]
remotefs-ftp = { version = "^0.2", features = ["native-tls"] }
remotefs-ssh = { version = "^0.6" }
remotefs-ftp = { version = "^0.3", features = ["native-tls"] }
remotefs-ssh = { version = "^0.7" }
[dev-dependencies]
pretty_assertions = "^1"

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Developed by <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Current version: 0.18.0 10/06/2025</p>
<p align="center">Current version: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -10,8 +10,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
posix: { target_family = "unix" },
win: { target_family = "windows" },
// exclusive features
smb: { all(feature = "smb", not( macos )) },
smb_unix: { all(unix, feature = "smb", not(macos)) },
smb: { feature = "smb" },
smb_unix: { all(unix, feature = "smb") },
smb_windows: { all(windows, feature = "smb") }
}

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Entwickelt von <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Aktuelle Version: 0.18.0 10/06/2025</p>
<p align="center">Aktuelle Version: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -10,6 +10,10 @@
- [Unterbefehle](#unterbefehle)
- [Ein Thema importieren](#ein-thema-importieren)
- [Neueste Version installieren](#neueste-version-installieren)
- [Unterbefehle](#unterbefehle-1)
- [Ein Theme importieren](#ein-theme-importieren)
- [Neueste Version installieren](#neueste-version-installieren-1)
- [SSH-Hosts importieren](#ssh-hosts-importieren)
- [S3-Verbindungsparameter](#s3-verbindungsparameter)
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen-)
- [Dateiexplorer 📂](#dateiexplorer-)
@@ -29,9 +33,9 @@
- [AWS S3 Adressargument](#aws-s3-adressargument-1)
- [SMB Adressargument](#smb-adressargument-1)
- [Wie das Passwort bereitgestellt werden kann 🔐](#wie-das-passwort-bereitgestellt-werden-kann--1)
- [Unterbefehle](#unterbefehle-1)
- [Unterbefehle](#unterbefehle-2)
- [Ein Thema importieren](#ein-thema-importieren-1)
- [Neueste Version installieren](#neueste-version-installieren-1)
- [Neueste Version installieren](#neueste-version-installieren-2)
- [S3-Verbindungsparameter](#s3-verbindungsparameter-1)
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen--1)
- [Dateiexplorer 📂](#dateiexplorer--1)
@@ -173,6 +177,22 @@ Führen Sie termscp als `termscp theme <thema-datei>` aus
Führen Sie termscp als `termscp update` aus
### Unterbefehle
#### Ein Theme importieren
Führen Sie termscp mit `termscp theme <theme-datei>` aus.
#### Neueste Version installieren
Führen Sie termscp mit `termscp update` aus.
#### SSH-Hosts importieren
Führen Sie termscp mit `termscp import-ssh-hosts [ssh-config-datei]` aus.
Importieren Sie alle Hosts aus der angegebenen SSH-Konfigurationsdatei (wenn keine angegeben ist, wird `~/.ssh/config` verwendet) als Lesezeichen in termscp. Identitätsdateien werden ebenfalls als SSH-Schlüssel in termscp importiert.
---
## S3-Verbindungsparameter
@@ -296,6 +316,7 @@ Diese Panels sind im Wesentlichen 3 (ja, tatsächlich drei):
| <CTRL+A> | Alle Dateien auswählen | |
| <ALT+A> | Alle Dateien abwählen | |
| <CTRL+C> | Dateiübertragungsvorgang abbrechen | |
| `<CTRL+S>` | Gesamte Größe des ausgewählten Pfads abrufen | Size |
| <CTRL+T> | Alle synchronisierten Pfade anzeigen | Track |
### Mit mehreren Dateien arbeiten 🥷

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Desarrollado por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Versión actual: 0.18.0 10/06/2025</p>
<p align="center">Versión actual: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -8,6 +8,10 @@
- [Argumento de dirección de WebDAV](#argumento-de-dirección-de-webdav)
- [Argumento dirección por SMB](#argumento-dirección-por-smb)
- [Cómo se puede proporcionar la contraseña 🔐](#cómo-se-puede-proporcionar-la-contraseña-)
- [Subcomandos](#subcomandos)
- [Importar un tema](#importar-un-tema)
- [Instalar la versión más reciente](#instalar-la-versión-más-reciente)
- [Importar hosts SSH](#importar-hosts-ssh)
- [S3 parámetros de conexión](#s3-parámetros-de-conexión)
- [Credenciales de S3 🦊](#credenciales-de-s3-)
- [Explorador de archivos 📂](#explorador-de-archivos-)
@@ -153,6 +157,22 @@ La contraseña se puede proporcionar básicamente a través de 3 formas cuando s
- Con `sshpass`: puede proporcionar la contraseña a través de `sshpass`, p. ej. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Se te pedirá que ingreses: si no utilizas ninguno de los métodos anteriores, se te pedirá la contraseña, como ocurre con las herramientas más clásicas como `scp`, `ssh`, etc.
### Subcomandos
#### Importar un tema
Ejecute termscp como `termscp theme <archivo-tema>`
#### Instalar la versión más reciente
Ejecute termscp como `termscp update`
#### Importar hosts SSH
Ejecute termscp como `termscp import-ssh-hosts [archivo-config-ssh]`
Importa todos los hosts del archivo de configuración SSH especificado (si no se proporciona, se usará `~/.ssh/config`) como marcadores en termscp. Los archivos de identidad también se importarán como claves SSH en termscp.
---
## S3 parámetros de conexión
@@ -231,25 +251,25 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
| `<BACKTAB>` | Cambiar entre la pestaña de registro y el explorador | |
| `<A>` | Alternar archivos ocultos | All |
| `<B>` | Ordenar archivos por | Bubblesort? |
| `<C|F5>` | Copiar archivo / directorio | Copy |
| `<D|F7>` | Hacer directorio | Directory |
| `<E|F8|DEL>` | Eliminar archivo | Erase |
| `<C\|F5>` | Copiar archivo / directorio | Copy |
| `<D\|F7>` | Hacer directorio | Directory |
| `<E\|F8\|DEL>` | Eliminar archivo | Erase |
| `<F>` | Búsqueda de archivos | Find |
| `<G>` | Ir a la ruta proporcionada | Go to |
| `<H|F1>` | Mostrar ayuda | Help |
| `<H\|F1>` | Mostrar ayuda | Help |
| `<I>` | Mostrar información sobre el archivo | Info |
| `<K>` | Crear un enlace simbólico que apunte a la entrada seleccionada actualmente | symlinK |
| `<L>` | Recargar contenido del directorio / Borrar selección | List |
| `<M>` | Seleccione un archivo | Mark |
| `<N>` | Crear un nuevo archivo con el nombre proporcionado | New |
| `<O|F4>` | Editar archivo | Open |
| `<O\|F4>` | Editar archivo | Open |
| `<P>` | Open log panel | Panel |
| `<Q|F10>` | Salir de termscp | Quit |
| `<R|F6>` | Renombrar archivo | Rename |
| `<S|F2>` | Guardar archivo como... | Save |
| `<Q\|F10>` | Salir de termscp | Quit |
| `<R\|F6>` | Renombrar archivo | Rename |
| `<S\|F2>` | Guardar archivo como... | Save |
| `<T>` | Sincronizar los cambios en la ruta seleccionada con el control remoto | Track |
| `<U>` | Ir al directorio principal | Upper |
| `<V|F3>` | Abrir archivo con el programa predeterminado | View |
| `<V\|F3>` | Abrir archivo con el programa predeterminado | View |
| `<W>` | Abrir archivo con el programa proporcionado | With |
| `<X>` | Ejecutar un comando | eXecute |
| `<Y>` | Alternar navegación sincronizada | sYnc |
@@ -258,6 +278,7 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
| `<CTRL+A>` | Seleccionar todos los archivos | |
| `<ALT+A>` | Deseleccionar todos los archivos | |
| `<CTRL+C>` | Abortar el proceso de transferencia de archivos | |
| `<CTRL+S>` | Obtener el tamaño total de la ruta seleccionada | Size |
| `<CTRL+T>` | Mostrar todas las rutas sincronizadas | Track |
### Trabajar con múltiples archivos 🥷

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Développé par <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Version actuelle: 0.18.0 10/06/2025</p>
<p align="center">Version actuelle: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -8,6 +8,10 @@
- [Argument d'adresse WebDAV](#argument-dadresse-webdav)
- [Argument d'adresse SMB](#argument-dadresse-smb)
- [Comment le mot de passe peut être fourni 🔐](#comment-le-mot-de-passe-peut-être-fourni-)
- [Sous-commandes](#sous-commandes)
- [Importer un thème](#importer-un-thème)
- [Installer la dernière version](#installer-la-dernière-version)
- [Importer des hôtes SSH](#importer-des-hôtes-ssh)
- [S3 paramètres de connexion](#s3-paramètres-de-connexion)
- [Identifiants S3 🦊](#identifiants-s3-)
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
@@ -142,7 +146,6 @@ syntaxe **Other systems**:
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### Comment le mot de passe peut être fourni 🔐
Vous avez probablement remarqué que, lorsque vous fournissez l'adresse comme argument, il n'y a aucun moyen de fournir le mot de passe.
@@ -152,6 +155,22 @@ Le mot de passe peut être fourni de 3 manières lorsque l'argument d'adresse es
- Avec `sshpass`: vous pouvez fournir un mot de passe via `sshpass`, par ex. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Il vous sera demandé : si vous n'utilisez aucune des méthodes précédentes, le mot de passe vous sera demandé, comme c'est le cas avec les outils plus classiques tels que `scp`, `ssh`, etc.
### Sous-commandes
#### Importer un thème
Exécutez termscp avec `termscp theme <fichier-thème>`
#### Installer la dernière version
Exécutez termscp avec `termscp update`
#### Importer des hôtes SSH
Exécutez termscp avec `termscp import-ssh-hosts [fichier-config-ssh]`
Importez tous les hôtes du fichier de configuration SSH spécifié (si non fourni, `~/.ssh/config` sera utilisé) comme favoris dans termscp. Les fichiers d'identité seront également importés comme clés SSH dans termscp.
---
## S3 paramètres de connexion
@@ -230,25 +249,25 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
| `<BACKTAB>` | Basculer entre l'onglet journal et l'explorateur | |
| `<A>` | Basculer les fichiers cachés | All |
| `<B>` | Trier les fichiers par | Bubblesort? |
| `<C|F5>` | Copier le fichier/répertoire | Copy |
| `<D|F7>` | Créer un dossier | Directory |
| `<E|F8|DEL>` | Supprimer le fichier (Identique à `DEL`) | Erase |
| `<C\|F5>` | Copier le fichier/répertoire | Copy |
| `<D\|F7>` | Créer un dossier | Directory |
| `<E\|F8\|DEL>` | Supprimer le fichier (Identique à `DEL`) | Erase |
| `<F>` | Rechercher des fichiers | Find |
| `<G>` | Aller au chemin fourni | Go to |
| `<H|F1>` | Afficher l'aide | Help |
| `<H\|F1>` | Afficher l'aide | Help |
| `<I>` | Afficher les informations sur le fichier ou le dossier sélectionné | Info |
| `<K>` | Créer un lien symbolique pointant vers l'entrée actuellement sélectionnée | symlinK |
| `<L>` | Recharger le contenu du répertoire actuel / Effacer la sélection | List |
| `<M>` | Sélectionner un fichier | Mark |
| `<N>` | Créer un nouveau fichier avec le nom fourni | New |
| `<O|F4>` | Modifier le fichier | Open |
| `<O\|F4>` | Modifier le fichier | Open |
| `<P>` | Ouvre le panel de journals | Panel |
| `<Q|F10>` | Quitter termscp | Quit |
| `<R|F6>` | Renommer le fichier | Rename |
| `<S|F2>` | Enregistrer le fichier sous... | Save |
| `<Q\|F10>` | Quitter termscp | Quit |
| `<R\|F6>` | Renommer le fichier | Rename |
| `<S\|F2>` | Enregistrer le fichier sous... | Save |
| `<T>` | Synchroniser les modifications apportées au chemin sélectionné | Track |
| `<U>` | Aller dans le répertoire parent | Upper |
| `<V|F3>` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View |
| `<V\|F3>` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View |
| `<W>` | Ouvrir le fichier avec le programme spécifié | With |
| `<X>` | Exécuter une commande | eXecute |
| `<Y>` | Basculer la navigation synchronisée | sYnc |
@@ -257,6 +276,7 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
| `<CTRL+A>` | Sélectionner tous les fichiers | |
| `<ALT+A>` | Desélectionner tous les fichiers | |
| `<CTRL+C>` | Abandonner le processus de transfert de fichiers | |
| `<CTRL+S>` | Obtenir la taille totale du chemin sélectionné | Size |
| `<CTRL+T>` | Afficher tous les chemins synchronisés | Track |
### Travailler sur plusieurs fichiers 🥷

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Sviluppato da <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Versione corrente: 0.18.0 10/06/2025</p>
<p align="center">Versione corrente: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -8,6 +8,10 @@
- [Argomento indirizzo per WebDAV](#argomento-indirizzo-per-webdav)
- [Indirizzo SMB](#indirizzo-smb)
- [Come fornire la password 🔐](#come-fornire-la-password-)
- [Sottocomandi](#sottocomandi)
- [Importare un tema](#importare-un-tema)
- [Installare lultima versione](#installare-lultima-versione)
- [Importare host SSH](#importare-host-ssh)
- [Parametri di connessione S3](#parametri-di-connessione-s3)
- [Credenziali S3 🦊](#credenziali-s3-)
- [File explorer 📂](#file-explorer-)
@@ -140,7 +144,6 @@ SMB ha una sintassi differente rispetto agli altri protocolli e cambia in base a
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### Come fornire la password 🔐
Quando si usa l'argomento indirizzo non è possibile fornire la password direttamente nell'argomento, esistono però altri metodi per farlo:
@@ -149,6 +152,22 @@ Quando si usa l'argomento indirizzo non è possibile fornire la password diretta
- Tramite `sshpass`: puoi fornire la password tramite l'applicazione GNU/Linux sshpass `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Forniscila quando richiesta: se non la fornisci tramite nessun metodo precedente, alla connessione ti verrà richiesto di fornirla in un prompt che la oscurerà (come avviene con sudo tipo).
### Sottocomandi
#### Importare un tema
Esegui termscp come `termscp theme <file-tema>`
#### Installare lultima versione
Esegui termscp come `termscp update`
#### Importare host SSH
Esegui termscp come `termscp import-ssh-hosts [file-config-ssh]`
Importa tutti gli host dal file di configurazione SSH specificato (se non fornito, verrà usato `~/.ssh/config`) come segnalibri in termscp. I file di identità verranno importati come chiavi SSH in termscp.
---
## Parametri di connessione S3
@@ -226,25 +245,25 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
| `<BACKTAB>` | Cambia tra explorer e pannello di log | |
| `<A>` | Mostra/nascondi file nascosti | All |
| `<B>` | Ordina file per | Bubblesort? |
| `<C|F5>` | Copia file/directory | Copy |
| `<D|F7>` | Crea directory | Directory |
| `<E|F8|DEL>` | Elimina file | Erase |
| `<C\|F5>` | Copia file/directory | Copy |
| `<D\|F7>` | Crea directory | Directory |
| `<E\|F8\|DEL>` | Elimina file | Erase |
| `<F>` | Cerca file (wild match supportato) | Find |
| `<G>` | Vai al percorso indicato | Go to |
| `<H|F1>` | Mostra help | Help |
| `<H\|F1>` | Mostra help | Help |
| `<I>` | Mostra informazioni per il file selezionato | Info |
| `<K>` | Crea un link simbolico che punta al file selezionato | symlinK |
| `<L>` | Ricarica posizione corrente / pulisci selezione file | List |
| `<M>` | Seleziona file | Mark |
| `<N>` | Crea nuovo file con il nome fornito | New |
| `<O|F4>` | Modifica file; Vedi text editor | Open |
| `<O\|F4>` | Modifica file; Vedi text editor | Open |
| `<P>` | Apri pannello log | Panel |
| `<Q|F10>` | Termina termscp | Quit |
| `<R|F6>` | Rinomina file | Rename |
| `<S|F2>` | Salva file con nome | Save |
| `<Q\|F10>` | Termina termscp | Quit |
| `<R\|F6>` | Rinomina file | Rename |
| `<S\|F2>` | Salva file con nome | Save |
| `<T>` | Sincronizza il percorso locale con l'host remoto | Track |
| `<U>` | Vai alla directory padre | Upper |
| `<V|F3>` | Apri il file con il programma definito dal sistema | View |
| `<V\|F3>` | Apri il file con il programma definito dal sistema | View |
| `<W>` | Apri il file con il programma specificato | With |
| `<X>` | Esegui comando shell | eXecute |
| `<Y>` | Abilita/disabilita Sync-Browsing | sYnc |
@@ -253,6 +272,7 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
| `<CTRL+A>` | Seleziona tutti i file | |
| `<ALT+A>` | Deseleziona tutti i file | |
| `<CTRL+C>` | Annulla trasferimento file | |
| `<CTRL+S>` | Ottieni la dimensione totale del percorso selezionato | Size |
| `<CTRL+T>` | Visualizza tutti i percorsi sincronizzati | Track |
### Lavora con più file 🥷

View File

@@ -11,6 +11,7 @@
- [Subcommands](#subcommands)
- [Import a theme](#import-a-theme)
- [Install latest version](#install-latest-version)
- [Import ssh hosts](#import-ssh-hosts)
- [S3 connection parameters](#s3-connection-parameters)
- [S3 credentials 🦊](#s3-credentials-)
- [File explorer 📂](#file-explorer-)
@@ -166,6 +167,12 @@ Run termscp as `termscp theme <theme-file>`
Run termscp as `termscp update`
#### Import ssh hosts
Run termscp as `termscp import-ssh-hosts [ssh-config-file]`
Import all the hosts from the specified ssh config file (if not provided, `~/.ssh/config` will be used) as bookmarks in termscp. Identity files will be imported as ssh keys in termscp too.
---
## S3 connection parameters
@@ -271,6 +278,7 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
| `<CTRL+A>` | Select all files | |
| `<ALT+A>` | Deselect all files | |
| `<CTRL+C>` | Abort file transfer process | |
| `<CTRL+S>` | Get total size of the selected path | Size |
| `<CTRL+T>` | Show all synchronized paths | Track |
### Work on multiple files 🥷

View File

@@ -71,7 +71,7 @@
</p>
<p align="center">Desenvolvido por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Versão atual: 0.18.0 10/06/2025</p>
<p align="center">Versão atual: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -11,6 +11,7 @@
- [Subcomandos](#subcomandos)
- [Importar um Tema](#importar-um-tema)
- [Instalar a Última Versão](#instalar-a-última-versão)
- [Importar hosts SSH](#importar-hosts-ssh)
- [Parâmetros de Conexão do S3](#parâmetros-de-conexão-do-s3)
- [Credenciais do S3 🦊](#credenciais-do-s3-)
- [Explorador de Arquivos 📂](#explorador-de-arquivos-)
@@ -164,6 +165,12 @@ Execute o termscp como `termscp theme <theme-file>`
Execute o termscp como `termscp update`
#### Importar hosts SSH
Execute o termscp como `termscp import-ssh-hosts [arquivo-config-ssh]`
Importe todos os hosts do arquivo de configuração SSH especificado (se não for fornecido, `~/.ssh/config` será usado) como favoritos no termscp. Os arquivos de identidade também serão importados como chaves SSH no termscp.
---
## Parâmetros de Conexão do S3
@@ -271,6 +278,7 @@ Para trocar de painel, você precisa pressionar `<LEFT>` para mover para o paine
| `<CTRL+A>` | Selecionar todos os arquivos | |
| `<ALT+A>` | Deselecionar todos os arquivos | |
| `<CTRL+C>` | Abortir processo de transferência de arquivo | |
| `<CTRL+S>` | Obter o tamanho total do caminho selecionado | | Size |
| `<CTRL+T>` | Mostrar todos os caminhos sincronizados | Track |
### Trabalhar com múltiplos arquivos 🥷

View File

@@ -71,7 +71,7 @@
</p>
<p align="center"><a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
<p align="center">当前版本: 0.18.0 10/06/2025</p>
<p align="center">当前版本: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"

View File

@@ -8,6 +8,10 @@
- [WebDAV 地址参数](#webdav-地址参数)
- [SMB 地址参数](#smb-地址参数)
- [如何输入密码](#如何输入密码)
- [子命令](#子命令)
- [导入主题](#导入主题)
- [安装最新版本](#安装最新版本)
- [导入 SSH 主机](#导入-ssh-主机)
- [S3 连接参数](#s3-连接参数)
- [Aws S3 凭证](#aws-s3-凭证)
- [文件浏览](#文件浏览)
@@ -149,6 +153,21 @@ smb://[username@]<server-name>[:port]/<share>[/path/.../]
- 通过 `sshpass`: 你可以通过 `sshpass` 传入密码, 例如: `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- 提示输入密码:如果你不使用前面的任何方法,你会被提示输入密码,就像 `scp`、`ssh` 等比较经典的工具上一样。
### 子命令
#### 导入主题
以 termscp theme <theme-file> 的方式运行 termscp。
#### 安装最新版本
以 termscp update 的方式运行 termscp。
#### 导入 SSH 主机
以 `termscp import-ssh-hosts [ssh-config-file]` 的方式运行 termscp。
从指定的 SSH 配置文件中导入所有主机(如果未提供,则使用 `~/.ssh/config`)作为 termscp 中的书签。身份文件也会作为 SSH 密钥导入到 termscp 中。
---
## S3 连接参数
@@ -226,25 +245,25 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
| `<BACKTAB>` | 在日志面板和管理器面板之间切换 | |
| `<A>` | 是否显示隐藏文件 | All |
| `<B>` | 按..排序 | Bubblesort? |
| `<C|F5>` | 复制文件(夹) | Copy |
| `<D|F7>` | 创建文件夹 | Directory |
| `<E|F8|DEL>` | 删除文件 | Erase |
| `<C\|F5>` | 复制文件(夹) | Copy |
| `<D\|F7>` | 创建文件夹 | Directory |
| `<E\|F8\|DEL>` | 删除文件 | Erase |
| `<F>` | 文件搜索 (支持通配符) | Find |
| `<G>` | 跳转到指定路径 | Go to |
| `<H|F1>` | 显示帮助 | Help |
| `<H\|F1>` | 显示帮助 | Help |
| `<I>` | 显示选中文件(夹)信息 | Info |
| `<K>` | 创建指向当前选定条目的符号链接 | symlinK |
| `<L>` | 刷新当前目录列表 / 清除选中状态 | List |
| `<M>` | 选中文件 | Mark |
| `<N>` | 使用键入的名称新建文件 | New |
| `<O|F4>` | 编辑文件;参考文本编辑器文档 | Open |
| `<O\|F4>` | 编辑文件;参考文本编辑器文档 | Open |
| `<P>` | 打开日志面板 | Panel |
| `<Q|F10>` | 退出termscp | Quit |
| `<R|F7>` | 重命名文件 | Rename |
| `<S|F2>` | 另存为... | Save |
| `<Q\|F10>` | 退出termscp | Quit |
| `<R\|F7>` | 重命名文件 | Rename |
| `<S\|F2>` | 另存为... | Save |
| `<T>` | 显示所有同步路径 | Track |
| `<U>` | 进入上层目录 | Upper |
| `<V|F3>` | 使用默认方式打开文件 | View |
| `<V\|F3>` | 使用默认方式打开文件 | View |
| `<W>` | 使用指定程序打开文件 | With |
| `<X>` | 运行命令 | eXecute |
| `<Y>` | 是否开启同步浏览 | sYnc |
@@ -253,6 +272,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
| `<CTRL+A>` | 选中所有文件 | |
| `<ALT+A>` | 取消选择所有文件 | |
| `<CTRL+C>` | 终止文件传输 | |
| `<CTRL+S>` | 获取所选路径的总大小 | Size |
| `<CTRL+T>` | 显示所有同步路径 | Track |
### 操作多个文件 🥷

View File

@@ -8,10 +8,10 @@
# -f, -y, --force, --yes
# Skip the confirmation prompt during installation
TERMSCP_VERSION="0.18.0"
TERMSCP_VERSION="0.19.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"
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_amd64.deb"
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_arm64.deb"
PATH="$PATH:/usr/sbin"
@@ -33,8 +33,8 @@ NO_COLOR="$(tput sgr0 2>/dev/null || printf '')"
set_termscp_version() {
TERMSCP_VERSION="$1"
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"
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_amd64.deb"
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_arm64.deb"
}
info() {

View File

@@ -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.18.0.nupkg"
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.19.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.18.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.19.0_amd64.deb</span>
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
</div>
<h3>

View File

@@ -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.18.0 is NOW out! Download it from</span>&nbsp;
<span translate="intro.versionAlert">termscp 0.19.0 is NOW out! Download it from</span>&nbsp;
<a href="/get-started.html" translate="intro.here">here!</a>
</p>
</div>

View File

@@ -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.18.0 is NOW out! Download it from",
"versionAlert": "termscp 0.19.0 is NOW out! Download it from",
"here": "here",
"features": {
"handy": {

View File

@@ -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.18.0 ya está disponible! Descárgalo desde",
"versionAlert": "termscp 0.19.0 ya está disponible! Descárgalo desde",
"here": "aquì",
"features": {
"handy": {

View File

@@ -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.18.0 est maintenant sorti! Télécharge-le depuis",
"versionAlert": "termscp 0.19.0 est maintenant sorti! Télécharge-le depuis",
"here": "ici",
"features": {
"handy": {

View File

@@ -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.18.0 è ORA disponbile! Scaricalo da",
"versionAlert": "termscp 0.19.0 è ORA disponbile! Scaricalo da",
"here": "qui",
"features": {
"handy": {

View File

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

View File

@@ -448,35 +448,7 @@ impl ActivityManager {
// -- misc
fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
// Get config dir
match environment::init_config_dir() {
Ok(path) => {
// If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system.
if let Some(config_dir_path) = path {
let bookmarks_file: PathBuf =
environment::get_bookmarks_paths(config_dir_path.as_path());
// Initialize client
BookmarksClient::new(
bookmarks_file.as_path(),
config_dir_path.as_path(),
16,
keyring,
)
.map(Option::Some)
.map_err(|e| {
format!(
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
bookmarks_file.display(),
config_dir_path.display(),
e
)
})
} else {
Ok(None)
}
}
Err(err) => Err(err),
}
crate::support::bookmarks_client(keyring)
}
/// Initialize configuration client

View File

@@ -15,6 +15,9 @@ use crate::system::logging::LogLevel;
pub enum Task {
Activity(NextActivity),
/// Import ssh hosts from the specified ssh config file, or from the default location
/// and save them as bookmarks.
ImportSshHosts(Option<PathBuf>),
ImportTheme(PathBuf),
InstallUpdate,
Version,
@@ -72,7 +75,8 @@ pub struct Args {
#[argh(subcommand)]
pub enum ArgsSubcommands {
Config(ConfigArgs),
LoadTheme(LoadThemeArgs),
ImportSshHosts(ImportSshHostsArgs),
ImportTheme(ImportThemeArgs),
Update(UpdateArgs),
}
@@ -86,10 +90,20 @@ pub struct ConfigArgs {}
#[argh(subcommand, name = "update")]
pub struct UpdateArgs {}
#[derive(FromArgs)]
/// import ssh hosts from the specified ssh config file, or from the default location
/// and save them as bookmarks.
#[argh(subcommand, name = "import-ssh-hosts")]
pub struct ImportSshHostsArgs {
#[argh(positional)]
/// optional ssh config file; if not specified, the default location will be used
pub ssh_config: Option<PathBuf>,
}
#[derive(FromArgs)]
/// import the specified theme
#[argh(subcommand, name = "theme")]
pub struct LoadThemeArgs {
pub struct ImportThemeArgs {
#[argh(positional)]
/// theme file
pub theme: PathBuf,
@@ -118,6 +132,14 @@ impl RunOpts {
}
}
pub fn import_ssh_hosts(ssh_config: Option<PathBuf>, keyring: bool) -> Self {
Self {
task: Task::ImportSshHosts(ssh_config),
keyring,
..Default::default()
}
}
pub fn import_theme(theme: PathBuf) -> Self {
Self {
task: Task::ImportTheme(theme),

View File

@@ -65,11 +65,11 @@ impl FileExplorerBuilder {
/// Set formatter for FileExplorer
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
if let Some(fmt_str) = fmt_str {
if let Some(e) = self.explorer.as_mut()
&& let Some(fmt_str) = fmt_str
{
e.fmt = Formatter::new(fmt_str);
}
}
self
}
}

View File

@@ -13,6 +13,10 @@ use remotefs_kube::KubeMultiPodFs as KubeFs;
use remotefs_smb::SmbOptions;
#[cfg(smb)]
use remotefs_smb::{SmbCredentials, SmbFs};
#[cfg(windows)]
use remotefs_ssh::LibSsh2Session as SshSession;
#[cfg(unix)]
use remotefs_ssh::LibSshSession as SshSession;
use remotefs_ssh::{ScpFs, SftpFs, SshAgentIdentity, SshConfigParseRule, SshOpts};
use remotefs_webdav::WebDAVFs;
@@ -138,12 +142,18 @@ impl RemoteFsBuilder {
}
/// Build scp client
fn scp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> ScpFs {
fn scp_client(
params: GenericProtocolParams,
config_client: &ConfigClient,
) -> ScpFs<SshSession> {
Self::build_ssh_opts(params, config_client).into()
}
/// Build sftp client
fn sftp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> SftpFs {
fn sftp_client(
params: GenericProtocolParams,
config_client: &ConfigClient,
) -> SftpFs<SshSession> {
Self::build_ssh_opts(params, config_client).into()
}
@@ -235,11 +245,11 @@ impl RemoteFsBuilder {
}
// For SSH protocols, only set password if explicitly provided and non-empty.
// This allows the SSH library to prioritize key-based and agent authentication.
if let Some(password) = params.password {
if !password.is_empty() {
if let Some(password) = params.password
&& !password.is_empty()
{
opts = opts.password(password);
}
}
if let Some(config_path) = config_client.get_ssh_config() {
opts = opts.config_file(
PathBuf::from(config_path),

View File

@@ -22,7 +22,7 @@ extern crate log;
extern crate magic_crypt;
use std::env;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::time::Duration;
use self::activity_manager::{ActivityManager, NextActivity};
@@ -72,7 +72,10 @@ fn main() -> MainResult<()> {
fn parse_args(args: Args) -> Result<RunOpts, String> {
let run_opts = match args.nested {
Some(ArgsSubcommands::Update(_)) => RunOpts::update(),
Some(ArgsSubcommands::LoadTheme(args)) => RunOpts::import_theme(args.theme),
Some(ArgsSubcommands::ImportSshHosts(subargs)) => {
RunOpts::import_ssh_hosts(subargs.ssh_config, !args.wno_keyring)
}
Some(ArgsSubcommands::ImportTheme(args)) => RunOpts::import_theme(args.theme),
Some(ArgsSubcommands::Config(_)) => RunOpts::config(),
None => {
let mut run_opts: RunOpts = RunOpts::default();
@@ -111,11 +114,11 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
};
// Local directory
if let Some(localdir) = run_opts.remote.local_dir.as_deref() {
if let Err(err) = env::set_current_dir(localdir) {
if let Some(localdir) = run_opts.remote.local_dir.as_deref()
&& let Err(err) = env::set_current_dir(localdir)
{
return Err(format!("Bad working directory argument: {err}"));
}
}
run_opts
}
@@ -127,6 +130,7 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
/// Run task and return rc
fn run(run_opts: RunOpts) -> MainResult<()> {
match run_opts.task {
Task::ImportSshHosts(ssh_config) => run_import_ssh_hosts(ssh_config, run_opts.keyring),
Task::ImportTheme(theme) => run_import_theme(&theme),
Task::InstallUpdate => run_install_update(),
Task::Activity(activity) => {
@@ -145,6 +149,17 @@ fn print_version() -> MainResult<()> {
Ok(())
}
fn run_import_ssh_hosts(ssh_config_path: Option<PathBuf>, keyring: bool) -> MainResult<()> {
support::import_ssh_hosts(ssh_config_path, keyring)
.map(|_| {
println!("SSH hosts have been successfully imported!");
})
.map_err(|err| {
eprintln!("{err}");
err.into()
})
}
fn run_import_theme(theme: &Path) -> MainResult<()> {
match support::import_theme(theme) {
Ok(_) => {

View File

@@ -2,11 +2,14 @@
//!
//! this module exposes some extra run modes for termscp, meant to be used for "support", such as installing themes
// mod
mod import_ssh_hosts;
use std::fs;
use std::path::{Path, PathBuf};
pub use self::import_ssh_hosts::import_ssh_hosts;
use crate::system::auto_update::{Update, UpdateStatus};
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient;
use crate::system::environment;
use crate::system::notifications::Notification;
@@ -83,3 +86,36 @@ fn get_config_client() -> Option<ConfigClient> {
}
}
}
/// Init [`BookmarksClient`].
pub fn bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
// Get config dir
match environment::init_config_dir() {
Ok(path) => {
// If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system.
if let Some(config_dir_path) = path {
let bookmarks_file: PathBuf =
environment::get_bookmarks_paths(config_dir_path.as_path());
// Initialize client
BookmarksClient::new(
bookmarks_file.as_path(),
config_dir_path.as_path(),
16,
keyring,
)
.map(Option::Some)
.map_err(|e| {
format!(
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
bookmarks_file.display(),
config_dir_path.display(),
e
)
})
} else {
Ok(None)
}
}
Err(err) => Err(err),
}
}

View File

@@ -0,0 +1,326 @@
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use ssh2_config::{Host, HostClause, ParseRule, SshConfig};
use crate::filetransfer::params::GenericProtocolParams;
use crate::filetransfer::{FileTransferParams, FileTransferProtocol, ProtocolParams};
/// Parameters required to add an ssh key for a host.
struct SshKeyParams {
host: String,
ssh_key: String,
username: String,
}
/// Import ssh hosts from the specified ssh config file, or from the default location
/// and save them as bookmarks.
pub fn import_ssh_hosts(ssh_config: Option<PathBuf>, keyring: bool) -> Result<(), String> {
// get config client
let mut cfg_client = super::get_config_client()
.ok_or_else(|| String::from("Could not import ssh hosts: could not load configuration"))?;
// resolve ssh_config
let ssh_config = ssh_config.or_else(|| cfg_client.get_ssh_config().map(PathBuf::from));
// load bookmarks client
let mut bookmarks_client = super::bookmarks_client(keyring)?
.ok_or_else(|| String::from("Could not import ssh hosts: could not load bookmarks"))?;
// load ssh config
let ssh_config = match ssh_config {
Some(p) => {
debug!("Importing ssh hosts from file: {}", p.display());
let mut reader = BufReader::new(
File::open(&p)
.map_err(|e| format!("Could not open ssh config file {}: {e}", p.display()))?,
);
SshConfig::default().parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
}
None => {
debug!("Importing ssh hosts from default location");
SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)
}
}
.map_err(|e| format!("Could not parse ssh config file: {e}"))?;
// iter hosts and add bookmarks
ssh_config
.get_hosts()
.iter()
.flat_map(host_to_params)
.for_each(|(name, params, identity_file_params)| {
debug!("Adding bookmark for host: {name} with params: {params:?}");
bookmarks_client.add_bookmark(name, params, false);
// add ssh key if any
if let Some(identity_file_params) = identity_file_params {
debug!(
"Host {host} has identity file, will add ssh key for it",
host = identity_file_params.host
);
if let Err(err) = cfg_client.add_ssh_key(
&identity_file_params.host,
&identity_file_params.username,
&identity_file_params.ssh_key,
) {
error!(
"Could not add ssh key for host {host}: {err}",
host = identity_file_params.host
);
}
}
});
// save bookmarks
if let Err(err) = bookmarks_client.write_bookmarks() {
return Err(format!(
"Could not save imported ssh hosts as bookmarks: {err}"
));
}
println!("Imported ssh hosts");
Ok(())
}
/// Tries to derive [`FileTransferParams`] from the specified ssh host.
fn host_to_params(
host: &Host,
) -> impl Iterator<Item = (String, FileTransferParams, Option<SshKeyParams>)> {
host.pattern
.iter()
.filter_map(|pattern| host_pattern_to_params(host, pattern))
}
/// Tries to derive [`FileTransferParams`] from the specified ssh host and pattern.
///
/// If `IdentityFile` is specified in the host parameters, it will be included in the returned tuple.
fn host_pattern_to_params(
host: &Host,
pattern: &HostClause,
) -> Option<(String, FileTransferParams, Option<SshKeyParams>)> {
debug!("Processing host with pattern: {pattern:?}",);
if pattern.negated || pattern.pattern.contains('*') || pattern.pattern.contains('?') {
debug!("Skipping host with pattern: {pattern}",);
return None;
}
let address = host
.params
.host_name
.as_deref()
.unwrap_or(pattern.pattern.as_str())
.to_string();
debug!("Resolved address for pattern {pattern}: {address}");
let port = host.params.port.unwrap_or(22);
debug!("Resolved port for pattern {pattern}: {port}");
let username = host.params.user.clone();
debug!("Resolved username for pattern {pattern}: {username:?}");
let identity_file_params = resolve_identity_file_path(host, pattern, &address);
Some((
pattern.to_string(),
FileTransferParams::new(
FileTransferProtocol::Sftp,
ProtocolParams::Generic(
GenericProtocolParams::default()
.address(address)
.port(port)
.username(username),
),
),
identity_file_params,
))
}
fn resolve_identity_file_path(
host: &Host,
pattern: &HostClause,
resolved_address: &str,
) -> Option<SshKeyParams> {
let (Some(username), Some(identity_file)) = (
host.params.user.as_ref(),
host.params.identity_file.as_ref().and_then(|v| v.first()),
) else {
debug!(
"No identity file specified for host {host}, skipping ssh key import",
host = pattern.pattern
);
return None;
};
// expand tilde
let identity_filepath = shellexpand::tilde(&identity_file.display().to_string()).to_string();
debug!("Resolved identity file for pattern {pattern}: {identity_filepath}",);
let Ok(mut ssh_file) = File::open(identity_file) else {
error!(
"Could not open identity file {identity_filepath} for host {host}",
host = pattern.pattern
);
return None;
};
let mut ssh_key = String::new();
use std::io::Read as _;
if let Err(err) = ssh_file.read_to_string(&mut ssh_key) {
error!(
"Could not read identity file {identity_filepath} for host {host}: {err}",
host = pattern.pattern
);
return None;
}
Some(SshKeyParams {
host: resolved_address.to_string(),
username: username.clone(),
ssh_key,
})
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use tempfile::NamedTempFile;
use super::*;
use crate::system::bookmarks_client::BookmarksClient;
#[test]
fn test_should_import_ssh_hosts() {
let ssh_test_config = ssh_test_config();
// import ssh hosts
let result = import_ssh_hosts(Some(ssh_test_config.config.path().to_path_buf()), false);
assert!(result.is_ok());
// verify imported hosts
let config_client = super::super::get_config_client()
.ok_or_else(|| String::from("Could not import ssh hosts: could not load configuration"))
.expect("failed to load config client");
// load bookmarks client
let bookmarks_client = super::super::bookmarks_client(false)
.expect("failed to load bookmarks client")
.expect("bookmarks client is none");
// verify bookmarks
check_bookmark(&bookmarks_client, "test1", "test1.example.com", 2200, None);
check_bookmark(
&bookmarks_client,
"test2",
"test2.example.com",
22,
Some("test2user"),
);
check_bookmark(
&bookmarks_client,
"test3",
"test3.example.com",
2222,
Some("test3user"),
);
// verify ssh keys
let (host, username, _key) = config_client
.get_ssh_key("test3user@test3.example.com")
.expect("ssh key is missing for test3user@test3.example.com");
assert_eq!(host, "test3.example.com");
assert_eq!(username, "test3user");
}
fn check_bookmark(
bookmarks_client: &BookmarksClient,
name: &str,
expected_address: &str,
expected_port: u16,
expected_username: Option<&str>,
) {
// verify bookmarks
let bookmark = bookmarks_client
.get_bookmark(name)
.expect("failed to get bookmark");
let params1 = bookmark
.params
.generic_params()
.expect("should have generic params");
assert_eq!(params1.address, expected_address);
assert_eq!(params1.port, expected_port);
assert_eq!(params1.username.as_deref(), expected_username);
assert!(params1.password.is_none());
}
struct SshTestConfig {
config: NamedTempFile,
#[allow(dead_code)]
identity_file: NamedTempFile,
}
fn ssh_test_config() -> SshTestConfig {
use std::io::Write as _;
let mut identity_file = NamedTempFile::new().expect("failed to create tempfile");
writeln!(
identity_file,
r"-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAxKyYUMRCNPlb4ZV1VMofrzApu2l3wgP4Ot9wBvHsw/+RMpcHIbQK
9iQqAVp8Z+M1fJyPXTKjoJtIzuCLF6Sjo0KI7/tFTh+yPnA5QYNLZOIRZb8skumL4gwHww
5Z942FDPuUDQ30C2mZR9lr3Cd5pA8S1ZSPTAV9QQHkpgoS8cAL8QC6dp3CJjUC8wzvXh3I
oN3bTKxCpM10KMEVuWO3lM4Nvr71auB9gzo1sFJ3bwebCZIRH01FROyA/GXRiaOtJFG/9N
nWWI/iG5AJzArKpLZNHIP+FxV/NoRH0WBXm9Wq5MrBYrD1NQzm+kInpS/2sXk3m1aZWqLm
HF2NKRXSbQAAA8iI+KSniPikpwAAAAdzc2gtcnNhAAABAQDErJhQxEI0+VvhlXVUyh+vMC
m7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VO
H7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAe
SmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndv
B5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkys
FisPU1DOb6QielL/axeTebVplaouYcXY0pFdJtAAAAAwEAAQAAAP8u3PFuTVV5SfGazwIm
MgNaux82iOsAT/HWFWecQAkqqrruUw5f+YajH/riV61NE9aq2qNOkcJrgpTWtqpt980GGd
SHWlgpRWQzfIooEiDk6Pk8RVFZsEykkDlJQSIu2onZjhi5A5ojHgZoGGabDsztSqoyOjPq
6WPvGYRiDAR3leBMyp1WufBCJqAsC4L8CjPJSmnZhc5a0zXkC9Syz74Fa08tdM7bGhtvP1
GmzuYxkgxHH2IFeoumUSBHRiTZayGuRUDel6jgEiUMxenaDKXe7FpYzMm9tQZA10Mm4LhK
5rP9nd2/KRTFRnfZMnKvtIRC9vtlSLBe14qw+4ZCl60AAACAf1kghlO3+HIWplOmk/lCL0
w75Zz+RdvueL9UuoyNN1QrUEY420LsixgWSeRPby+Rb/hW+XSAZJQHowQ8acFJhU85So7f
4O4wcDuE4f6hpsW9tTfkCEUdLCQJ7EKLCrod6jIV7hvI6rvXiVucRpeAzdOaq4uzj2cwDd
tOdYVsnmQAAACBAOVxBsvO/Sr3rZUbNtA6KewZh/09HNGoKNaCeiD7vaSn2UJbbPRByF/o
Oo5zv8ee8r3882NnmG808XfSn7pPZAzbbTmOaJt0fmyZhivCghSNzV6njW3o0PdnC0fGZQ
ruVXgkd7RJFbsIiD4dDcF4VCjwWHfTK21EOgJUA5pN6TNvAAAAgQDbcJWRx8Uyhkj2+srb
3n2Rt6CR7kEl9cw17ItFjMn+pO81/5U2aGw0iLlX7E06TAMQC+dyW/WaxQRey8RRdtbJ1e
TNKCN34QCWkyuYRHGhcNc0quEDayPw5QWGXlP4BzjfRUcPxY9cCXLe5wDLYsX33HwOAc59
RorU9FCmS/654wAAABFyb290QDhjNTBmZDRjMzQ1YQECAw==
-----END OPENSSH PRIVATE KEY-----"
)
.expect("failed to write identity file");
let mut file = NamedTempFile::new().expect("failed to create tempfile");
// let's declare a couple of hosts
writeln!(
file,
r#"
Host test1
HostName test1.example.com
Port 2200
Host test2
HostName test2.example.com
User test2user
Host test3
HostName test3.example.com
User test3user
Port 2222
IdentityFile {identity_path}
"#,
identity_path = identity_file.path().display()
)
.expect("failed to write ssh config");
SshTestConfig {
config: file,
identity_file,
}
}
}

View File

@@ -2,6 +2,8 @@
//!
//! Automatic update module. This module is used to upgrade the current version of termscp to the latest available on Github
use std::net::ToSocketAddrs as _;
use self_update::backends::github::Update as GithubUpdater;
pub use self_update::errors::Error as UpdateError;
use self_update::update::Release as UpdRelease;
@@ -67,6 +69,9 @@ impl Update {
/// otherwise if no version is available, return None
/// In case of error returns Error with the error description
pub fn is_new_version_available() -> Result<Option<Release>, UpdateError> {
// check if api.github.com is reachable before doing anything
Self::check_github_api_reachable()?;
info!("Checking whether a new version is available...");
GithubUpdater::configure()
// Set default options
@@ -83,6 +88,27 @@ impl Update {
.map(Self::check_version)
}
/// Check if api.github.com is reachable
/// This is useful to avoid long timeouts when the network is down
/// or the DNS is not working
fn check_github_api_reachable() -> Result<(), UpdateError> {
let Some(socket_addr) = ("api.github.com", 443)
.to_socket_addrs()
.ok()
.and_then(|mut i| i.next())
else {
error!("Could not resolve api.github.com");
return Err(UpdateError::Network(
"Could not resolve api.github.com".into(),
));
};
// just try to open a connection to api.github.com with a timeout of 5 seconds with tcp
std::net::TcpStream::connect_timeout(&socket_addr, std::time::Duration::from_secs(5))
.map(|_| ())
.map_err(|e| UpdateError::Network(format!("Could not reach api.github.com: {e}")))
}
/// 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);
@@ -212,4 +238,9 @@ mod test {
assert!(!Update::is_new_version_higher("0.9.9", "0.10.1"));
assert!(!Update::is_new_version_higher("0.10.9", "0.11.0"));
}
#[test]
fn test_should_check_whether_github_api_is_reachable() {
assert!(Update::check_github_api_reachable().is_ok());
}
}

View File

@@ -300,19 +300,18 @@ impl ConfigClient {
/// Get ssh key from host.
/// None is returned if key doesn't exist
/// `std::io::Error` is returned in case it was not possible to read the key file
pub fn get_ssh_key(&self, mkey: &str) -> std::io::Result<Option<SshHost>> {
pub fn get_ssh_key(&self, mkey: &str) -> Option<SshHost> {
if self.degraded {
return Ok(None);
return None;
}
// Check if Key exists
match self.config.remote.ssh_keys.get(mkey) {
None => Ok(None),
None => None,
Some(key_path) => {
// Get host and username
let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
// Return key
Ok(Some((host, username, PathBuf::from(key_path))))
Some((host, username, PathBuf::from(key_path)))
}
}
}
@@ -451,7 +450,7 @@ mod tests {
// I/O
assert!(client.add_ssh_key("Omar", "omar", "omar").is_err());
assert!(client.del_ssh_key("omar", "omar").is_err());
assert!(client.get_ssh_key("omar").ok().unwrap().is_none());
assert!(client.get_ssh_key("omar").is_none());
assert!(client.write_config().is_err());
assert!(client.read_config().is_err());
}
@@ -493,7 +492,7 @@ mod tests {
let mut expected_key_path: PathBuf = key_path;
expected_key_path.push("pi@192.168.1.31.key");
assert_eq!(
client.get_ssh_key("pi@192.168.1.31").unwrap().unwrap(),
client.get_ssh_key("pi@192.168.1.31").unwrap(),
(
String::from("192.168.1.31"),
String::from("pi"),
@@ -684,7 +683,7 @@ mod tests {
);
// Iterate keys
for key in client.iter_ssh_keys() {
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap();
let host: SshHost = client.get_ssh_key(key).unwrap();
assert_eq!(host.0, String::from("192.168.1.31"));
assert_eq!(host.1, String::from("pi"));
let mut expected_key_path: PathBuf = key_path.clone();
@@ -699,7 +698,7 @@ mod tests {
assert_eq!(key, rsa_key);
}
// Unexisting key
assert!(client.get_ssh_key("test").ok().unwrap().is_none());
assert!(client.get_ssh_key("test").is_none());
// Delete key
assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok());
}

View File

@@ -103,17 +103,11 @@ impl From<&ConfigClient> for SshKeyStorage {
// Iterate over keys in storage
for key in cfg_client.iter_ssh_keys() {
match cfg_client.get_ssh_key(key) {
Ok(host) => match host {
Some((addr, username, rsa_key_path)) => {
let key_name: String = Self::make_mapkey(&addr, &username);
hosts.insert(key_name, rsa_key_path);
}
None => continue,
},
Err(err) => {
error!("Failed to get SSH key for {}: {}", key, err);
continue;
}
}
info!("Got SSH key for {}", key);
}

View File

@@ -30,8 +30,9 @@ impl AuthActivity {
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) {
if let Some(key) = self.bookmarks_list.get(idx)
&& let Some(bookmark) = bookmarks_cli.get_bookmark(key)
{
// Load parameters into components
match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
@@ -40,7 +41,6 @@ impl AuthActivity {
}
}
}
}
/// Save current input fields as a bookmark
pub(super) fn save_bookmark(&mut self, form_tab: FormTab, name: String, save_password: bool) {
@@ -99,8 +99,9 @@ impl AuthActivity {
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) {
if let Some(key) = self.recents_list.get(idx)
&& let Some(bookmark) = client.get_recent(key)
{
// Load parameters
match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
@@ -109,7 +110,6 @@ impl AuthActivity {
}
}
}
}
/// Save current input fields as a "recent"
pub(super) fn save_recent(&mut self) {
@@ -129,12 +129,12 @@ impl AuthActivity {
/// Write bookmarks to file
fn write_bookmarks(&mut self) {
if let Some(bookmarks_cli) = self.bookmarks_client() {
if let Err(err) = bookmarks_cli.write_bookmarks() {
if let Some(bookmarks_cli) = self.bookmarks_client()
&& let Err(err) = bookmarks_cli.write_bookmarks()
{
self.mount_error(format!("Could not write bookmarks: {err}").as_str());
}
}
}
/// Initialize bookmarks client
pub(super) fn init_bookmarks_client(&mut self) {

View File

@@ -223,10 +223,7 @@ impl AuthActivity {
}
Err(err) => {
// Report error
error!("Failed to get latest version: {}", err);
self.mount_error(
format!("Could not check for new updates: {err}").as_str(),
);
error!("Failed to get latest version: {err}",);
}
}
} else {

View File

@@ -126,15 +126,15 @@ impl AuthActivity {
self.host_bridge_protocol = protocol;
// Update port
let port: u16 = self.get_input_port(FormTab::HostBridge);
if let HostBridgeProtocol::Remote(remote_protocol) = protocol {
if Self::is_port_standard(port) {
if let HostBridgeProtocol::Remote(remote_protocol) = protocol
&& 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

View File

@@ -687,8 +687,9 @@ impl AuthActivity {
/// mount release notes text area
pub(super) fn mount_release_notes(&mut self) {
if let Some(ctx) = self.context.as_ref() {
if let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES) {
if let Some(ctx) = self.context.as_ref()
&& let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES)
{
// make spans
let info_color = self.theme().misc_info_dialog;
assert!(
@@ -712,7 +713,6 @@ impl AuthActivity {
assert!(self.app.active(&Id::InstallUpdatePopup).is_ok());
}
}
}
/// Umount release notes text area
pub(super) fn umount_release_notes(&mut self) {

View File

@@ -0,0 +1,94 @@
use remotefs::File;
use super::{FileTransferActivity, LogLevel};
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
#[derive(Debug, Copy, Clone)]
enum Host {
HostBridge,
Remote,
}
impl FileTransferActivity {
pub(crate) fn action_get_file_size(&mut self) {
// Get selected file
self.mount_blocking_wait("Getting total path size...");
let total_size = match self.browser.tab() {
FileExplorerTab::HostBridge => {
let files = self.get_local_selected_entries().get_files();
self.get_files_size(files, Host::HostBridge)
}
FileExplorerTab::Remote => {
let files = self.get_remote_selected_entries().get_files();
self.get_files_size(files, Host::Remote)
}
FileExplorerTab::FindHostBridge => {
let files = self.get_found_selected_entries().get_files();
self.get_files_size(files, Host::HostBridge)
}
FileExplorerTab::FindRemote => {
let files = self.get_found_selected_entries().get_files();
self.get_files_size(files, Host::Remote)
}
};
self.umount_wait();
self.mount_info(format!(
"Total file size: {size}",
size = bytesize::ByteSize::b(total_size)
));
}
fn get_files_size(&mut self, files: Vec<File>, host: Host) -> u64 {
files.into_iter().map(|f| self.get_file_size(f, host)).sum()
}
fn get_file_size(&mut self, file: File, host: Host) -> u64 {
if let Some(symlink) = &file.metadata().symlink {
// stat
let stat_res = match host {
Host::HostBridge => self.host_bridge.stat(symlink).map_err(|e| e.to_string()),
Host::Remote => self.client.stat(symlink).map_err(|e| e.to_string()),
};
match stat_res {
Ok(stat) => stat.metadata().size,
Err(err_msg) => {
self.log(
LogLevel::Error,
format!(
"Failed to stat symlink target {path}: {err_msg}",
path = symlink.display(),
),
);
0
}
}
} else if file.is_dir() {
// list and sum
let list_res = match host {
Host::HostBridge => self
.host_bridge
.list_dir(&file.path)
.map_err(|e| e.to_string()),
Host::Remote => self.client.list_dir(&file.path).map_err(|e| e.to_string()),
};
match list_res {
Ok(list) => list.into_iter().map(|f| self.get_file_size(f, host)).sum(),
Err(err_msg) => {
self.log(
LogLevel::Error,
format!(
"Failed to list directory {path}: {err_msg}",
path = file.path.display(),
),
);
0
}
}
} else {
file.metadata().size
}
}
}

View File

@@ -99,24 +99,16 @@ impl FileTransferActivity {
// Iter files
match self.browser.tab() {
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
if self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&File> = entries
.iter()
.filter(|(x, dest_path)| {
self.remote_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
entries,
) = self.get_files_to_transfer_with_overwrites(
entries,
super::save::CheckFileExists::Remote,
)
})
.map(|(x, _)| x)
.collect();
// Check whether to replace files
if !existing_files.is_empty()
&& !self.should_replace_files(existing_files)
{
else {
debug!("User cancelled file transfer due to overwrites");
return;
}
}
};
if let Err(err) = self.filetransfer_send(
TransferPayload::TransferQueue(entries),
dest_path.as_path(),
@@ -131,24 +123,16 @@ impl FileTransferActivity {
}
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
if self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&File> = entries
.iter()
.filter(|(x, dest_path)| {
self.host_bridge_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
entries,
) = self.get_files_to_transfer_with_overwrites(
entries,
super::save::CheckFileExists::HostBridge,
)
})
.map(|(x, _)| x)
.collect();
// Check whether to replace files
if !existing_files.is_empty()
&& !self.should_replace_files(existing_files)
{
else {
debug!("User cancelled file transfer due to overwrites");
return;
}
}
};
if let Err(err) = self.filetransfer_recv(
TransferPayload::TransferQueue(entries),
dest_path.as_path(),

View File

@@ -23,6 +23,7 @@ pub(crate) mod copy;
pub(crate) mod delete;
pub(crate) mod edit;
pub(crate) mod exec;
pub(crate) mod file_size;
pub(crate) mod filter;
pub(crate) mod find;
pub(crate) mod mark;

View File

@@ -10,6 +10,37 @@ use super::{
TransferPayload,
};
enum GetFileToReplaceResult {
Replace(Vec<(File, PathBuf)>),
Cancel,
}
/// Result of getting files to transfer with overwrites.
///
/// - FilesToTransfer: files to transfer.
/// - Cancel: user cancelled the operation.
pub(crate) enum TransferFilesWithOverwritesResult {
FilesToTransfer(Vec<(File, PathBuf)>),
Cancel,
}
/// Decides whether to check file existence on host bridge or remote side.
pub(crate) enum CheckFileExists {
HostBridge,
Remote,
}
/// Options for all files replacement.
///
/// - ReplaceAll: user wants to replace all files.
/// - SkipAll: user wants to skip all files.
/// - Unset: no option set yet.
enum AllOpts {
ReplaceAll,
SkipAll,
Unset,
}
impl FileTransferActivity {
pub(crate) fn action_local_saveas(&mut self, input: String) {
self.local_send_file(TransferOpts::default().save_as(Some(input)));
@@ -60,22 +91,12 @@ impl FileTransferActivity {
dest_path.push(save_as);
}
// Iter files
if self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&File> = entries
.iter()
.filter(|(x, dest_path)| {
self.remote_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
)
})
.map(|(x, _)| x)
.collect();
// Check whether to replace files
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
let TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
self.get_files_to_transfer_with_overwrites(entries, CheckFileExists::Remote)
else {
debug!("User cancelled file transfer due to overwrites");
return;
}
}
};
if let Err(err) = self.filetransfer_send(
TransferPayload::TransferQueue(entries),
dest_path.as_path(),
@@ -128,23 +149,13 @@ impl FileTransferActivity {
if let Some(save_as) = opts.save_as {
dest_path.push(save_as);
}
// Iter files
if self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&File> = entries
.iter()
.filter(|(x, dest_path)| {
self.host_bridge_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
)
})
.map(|(x, _)| x)
.collect();
// Check whether to replace files
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
let TransferFilesWithOverwritesResult::FilesToTransfer(entries) = self
.get_files_to_transfer_with_overwrites(entries, CheckFileExists::HostBridge)
else {
debug!("User cancelled file transfer due to overwrites");
return;
}
}
};
if let Err(err) = self.filetransfer_recv(
TransferPayload::TransferQueue(entries),
dest_path.as_path(),
@@ -172,11 +183,17 @@ impl FileTransferActivity {
self.mount_radio_replace(&file_name);
// Wait for answer
trace!("Asking user whether he wants to replace file {}", file_name);
if self.wait_for_pending_msg(&[
Msg::PendingAction(PendingActionMsg::CloseReplacePopups),
Msg::PendingAction(PendingActionMsg::TransferPendingFile),
]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile)
{
if matches!(
self.wait_for_pending_msg(&[
Msg::PendingAction(PendingActionMsg::ReplaceCancel),
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite),
Msg::PendingAction(PendingActionMsg::ReplaceSkip),
Msg::PendingAction(PendingActionMsg::ReplaceSkipAll),
Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll),
]),
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)
| Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll)
) {
trace!("User wants to replace file");
self.umount_radio_replace();
true
@@ -187,28 +204,76 @@ impl FileTransferActivity {
}
}
/// Set pending transfer for many files into storage and mount radio
pub(crate) fn should_replace_files(&mut self, files: Vec<&File>) -> bool {
let file_names: Vec<String> = files.iter().map(|x| x.name()).collect();
self.mount_radio_replace_many(file_names.as_slice());
// Wait for answer
/// Get files to replace
fn get_files_to_replace(&mut self, files: Vec<(File, PathBuf)>) -> GetFileToReplaceResult {
// keep only files the user want to replace
let mut files_to_replace = vec![];
let mut all_opts = AllOpts::Unset;
for (file, p) in files {
// Check for all opts
match all_opts {
AllOpts::ReplaceAll => {
trace!(
"Asking user whether he wants to replace files {:?}",
file_names
"User wants to replace all files, including file {}",
file.name()
);
if self.wait_for_pending_msg(&[
Msg::PendingAction(PendingActionMsg::CloseReplacePopups),
Msg::PendingAction(PendingActionMsg::TransferPendingFile),
]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile)
{
trace!("User wants to replace files");
self.umount_radio_replace();
true
} else {
trace!("The user doesn't want replace file");
self.umount_radio_replace();
false
files_to_replace.push((file, p));
continue;
}
AllOpts::SkipAll => {
trace!(
"User wants to skip all files, including file {}",
file.name()
);
continue;
}
AllOpts::Unset => {}
}
let file_name = file.name();
self.mount_radio_replace(&file_name);
// Wait for answer
match self.wait_for_pending_msg(&[
Msg::PendingAction(PendingActionMsg::ReplaceCancel),
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite),
Msg::PendingAction(PendingActionMsg::ReplaceSkip),
Msg::PendingAction(PendingActionMsg::ReplaceSkipAll),
Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll),
]) {
Msg::PendingAction(PendingActionMsg::ReplaceCancel) => {
trace!("The user cancelled the replace operation");
self.umount_radio_replace();
return GetFileToReplaceResult::Cancel;
}
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite) => {
trace!("User wants to replace file {}", file_name);
files_to_replace.push((file, p));
}
Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll) => {
trace!(
"User wants to replace all files from now on, including file {}",
file_name
);
files_to_replace.push((file, p));
all_opts = AllOpts::ReplaceAll;
}
Msg::PendingAction(PendingActionMsg::ReplaceSkip) => {
trace!("The user skipped file {}", file_name);
}
Msg::PendingAction(PendingActionMsg::ReplaceSkipAll) => {
trace!(
"The user skipped all files from now on, including file {}",
file_name
);
all_opts = AllOpts::SkipAll;
}
_ => {}
}
self.umount_radio_replace();
}
GetFileToReplaceResult::Replace(files_to_replace)
}
/// Get file to check for path
@@ -224,4 +289,40 @@ impl FileTransferActivity {
p.push(e.name());
p
}
/// Get the files to transfer with overwrites.
///
/// Existing and unexisting files are splitted, and only existing files are prompted for replacement.
pub(crate) fn get_files_to_transfer_with_overwrites(
&mut self,
files: Vec<(File, PathBuf)>,
file_exists: CheckFileExists,
) -> TransferFilesWithOverwritesResult {
if !self.config().get_prompt_on_file_replace() {
return TransferFilesWithOverwritesResult::FilesToTransfer(files);
}
// unzip between existing and non-existing files
let (existing_files, new_files): (Vec<_>, Vec<_>) =
files.into_iter().partition(|(x, dest_path)| {
let p = Self::file_to_check_many(x, dest_path);
match file_exists {
CheckFileExists::Remote => self.remote_file_exists(p.as_path()),
CheckFileExists::HostBridge => self.host_bridge_file_exists(p.as_path()),
}
});
// filter only files to replace
let existing_files = match self.get_files_to_replace(existing_files) {
GetFileToReplaceResult::Replace(files) => files,
GetFileToReplaceResult::Cancel => {
return TransferFilesWithOverwritesResult::Cancel;
}
};
// merge back
TransferFilesWithOverwritesResult::FilesToTransfer(
existing_files.into_iter().chain(new_files).collect(),
)
}
}

View File

@@ -21,9 +21,8 @@ pub use popups::{
ATTR_FILES, ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, FatalPopup,
FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList,
WatcherPopup,
SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, SymlinkPopup,
SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList, WatcherPopup,
};
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};

View File

@@ -644,6 +644,11 @@ impl KeybindingsPopup {
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color))
.add_col(TextSpan::from(" Interrupt file transfer"))
.add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(key_color))
.add_col(TextSpan::from(
" Get total path size of selected files",
))
.add_row()
.add_col(TextSpan::new("<CTRL+T>").bold().fg(key_color))
.add_col(TextSpan::from(" Show watched paths"))
.build(),
@@ -1196,7 +1201,7 @@ impl ReplacePopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.choices(["Yes", "No"])
.choices(["Replace", "Skip", "Replace All", "Skip All", "Cancel"])
.title(text, Alignment::Center),
}
}
@@ -1205,9 +1210,6 @@ impl ReplacePopup {
impl Component<Msg, NoUserEvent> for ReplacePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
Some(Msg::Ui(UiMsg::ReplacePopupTabbed))
}
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
@@ -1221,102 +1223,36 @@ impl Component<Msg, NoUserEvent> for ReplacePopup {
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
}
Event::Keyboard(KeyEvent {
code: Key::Char('y'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::PendingAction(PendingActionMsg::TransferPendingFile)),
}) => Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)),
Event::Keyboard(KeyEvent {
code: Key::Char('n'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups)),
}) => Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)),
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::PendingAction(PendingActionMsg::TransferPendingFile))
} else {
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
}) => match self.perform(Cmd::Submit) {
CmdResult::Submit(State::One(StateValue::Usize(0))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite))
}
CmdResult::Submit(State::One(StateValue::Usize(1))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip))
}
_ => None,
CmdResult::Submit(State::One(StateValue::Usize(2))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll))
}
CmdResult::Submit(State::One(StateValue::Usize(3))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkipAll))
}
CmdResult::Submit(State::One(StateValue::Usize(4))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
}
#[derive(MockComponent)]
pub struct ReplacingFilesListPopup {
component: List,
}
impl ReplacingFilesListPopup {
pub fn new(files: &[String], color: Color) -> Self {
Self {
component: List::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.scroll(true)
.step(4)
.highlighted_color(color)
.highlighted_str("")
.title(
"The following files are going to be replaced",
Alignment::Center,
)
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
}
}
}
impl Component<Msg, NoUserEvent> for ReplacingFilesListPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
}
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
Some(Msg::Ui(UiMsg::ReplacePopupTabbed))
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
_ => Some(Msg::None),
},
_ => None,
}
}

View File

@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use tui_realm_stdlib::Input;
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
@@ -158,7 +158,7 @@ impl OwnStates {
.unwrap_or_else(|| PathBuf::from("/"));
// if path is `.`, then return None
if parent == PathBuf::from(".") {
if parent == Path::new(".") {
return Suggestion::None;
}

View File

@@ -192,6 +192,10 @@ impl ExplorerFuzzy {
code: Key::Char('i'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Transfer(TransferMsg::GetFileSize)),
Event::Keyboard(KeyEvent {
code: Key::Char('s') | Key::Function(2),
modifiers: KeyModifiers::NONE,
@@ -338,6 +342,10 @@ impl Component<Msg, NoUserEvent> for ExplorerFind {
code: Key::Char('i'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Transfer(TransferMsg::GetFileSize)),
Event::Keyboard(KeyEvent {
code: Key::Char('s') | Key::Function(2),
modifiers: KeyModifiers::NONE,
@@ -528,6 +536,10 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
code: Key::Char('r') | Key::Function(6),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowRenamePopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Transfer(TransferMsg::GetFileSize)),
Event::Keyboard(KeyEvent {
code: Key::Char('s') | Key::Function(2),
modifiers: KeyModifiers::NONE,
@@ -742,6 +754,10 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
code: Key::Char('r') | Key::Function(6),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowRenamePopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Transfer(TransferMsg::GetFileSize)),
Event::Keyboard(KeyEvent {
code: Key::Char('s') | Key::Function(2),
modifiers: KeyModifiers::NONE,

View File

@@ -72,7 +72,6 @@ enum Id {
QuitPopup,
RenamePopup,
ReplacePopup,
ReplacingFilesListPopup,
SaveAsPopup,
SortingPopup,
StatusBarHostBridge,
@@ -98,10 +97,14 @@ enum Msg {
#[derive(Debug, PartialEq)]
enum PendingActionMsg {
CloseReplacePopups,
CloseSyncBrowsingMkdirPopup,
MakePendingDirectory,
TransferPendingFile,
/// Replace file popup
ReplaceCancel,
ReplaceOverwrite,
ReplaceOverwriteAll,
ReplaceSkip,
ReplaceSkipAll,
}
#[derive(Debug, PartialEq)]
@@ -114,6 +117,7 @@ enum TransferMsg {
DeleteFile,
EnterDirectory,
ExecuteCmd(String),
GetFileSize,
GoTo(String),
GoToParentDirectory,
GoToPreviousDirectory,
@@ -171,8 +175,8 @@ enum UiMsg {
MarkAll,
/// Clear all marks
MarkClear,
Quit,
ReplacePopupTabbed,
ShowChmodPopup,
ShowCopyPopup,
ShowDeletePopup,
@@ -506,11 +510,11 @@ impl Activity for FileTransferActivity {
/// This function must be called once before terminating the activity.
fn on_destroy(&mut self) -> Option<Context> {
// Destroy cache
if let Some(cache) = self.cache.take() {
if let Err(err) = cache.close() {
if let Some(cache) = self.cache.take()
&& let Err(err) = cache.close()
{
error!("Failed to delete cache: {}", err);
}
}
// Disable raw mode
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);

View File

@@ -1260,7 +1260,10 @@ impl FileTransferActivity {
/// Get total size of transfer for host_bridgehost
fn get_total_transfer_size_host(&mut self, entry: &File) -> usize {
if entry.is_dir() {
// mount message to tell we are calculating size
self.mount_blocking_wait("Calculating transfer size…");
let sz = if entry.is_dir() {
// List dir
match self.host_bridge.list_dir(entry.path()) {
Ok(files) => files
@@ -1281,12 +1284,18 @@ impl FileTransferActivity {
}
} else {
entry.metadata.size as usize
}
};
self.umount_wait();
sz
}
/// Get total size of transfer for remote host
fn get_total_transfer_size_remote(&mut self, entry: &File) -> usize {
if entry.is_dir() {
// mount message to tell we are calculating size
self.mount_blocking_wait("Calculating transfer size…");
let sz = if entry.is_dir() {
// List directory
match self.client.list_dir(entry.path()) {
Ok(files) => files
@@ -1307,7 +1316,11 @@ impl FileTransferActivity {
}
} else {
entry.metadata.size as usize
}
};
self.umount_wait();
sz
}
// file changed

View File

@@ -5,7 +5,6 @@
// locals
// externals
use remotefs::fs::File;
use tuirealm::props::{AttrValue, Attribute};
use tuirealm::{State, StateValue, Update};
use super::actions::SelectedFile;
@@ -153,6 +152,9 @@ impl FileTransferActivity {
_ => panic!("Found tab doesn't support EXEC"),
};
}
TransferMsg::GetFileSize => {
self.action_get_file_size();
}
TransferMsg::GoTo(dir) => {
match self.browser.tab() {
FileExplorerTab::HostBridge => self.action_change_local_dir(dir),
@@ -504,15 +506,6 @@ impl FileTransferActivity {
self.disconnect_and_quit();
self.umount_quit();
}
UiMsg::ReplacePopupTabbed => {
if let Ok(Some(AttrValue::Flag(true))) =
self.app.query(&Id::ReplacePopup, Attribute::Focus)
{
assert!(self.app.active(&Id::ReplacingFilesListPopup).is_ok());
} else {
assert!(self.app.active(&Id::ReplacePopup).is_ok());
}
}
UiMsg::ShowChmodPopup => {
let selected_file = match self.browser.tab() {
#[cfg(posix)]

View File

@@ -269,29 +269,10 @@ impl FileTransferActivity {
// 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.area());
f.render_widget(Clear, popup);
let popup_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(85), // List
Constraint::Percentage(15), // Radio
]
.as_ref(),
)
.split(popup);
self.app
.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.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.area());
f.render_widget(Clear, popup);
@@ -944,37 +925,8 @@ impl FileTransferActivity {
assert!(self.app.active(&Id::ReplacePopup).is_ok());
}
pub(super) fn mount_radio_replace_many(&mut self, files: &[String]) {
let warn_color = self.theme().misc_warn_dialog;
assert!(
self.app
.remount(
Id::ReplacingFilesListPopup,
Box::new(components::ReplacingFilesListPopup::new(files, warn_color)),
vec![],
)
.is_ok()
);
assert!(
self.app
.remount(
Id::ReplacePopup,
Box::new(components::ReplacePopup::new(None, warn_color)),
vec![],
)
.is_ok()
);
assert!(self.app.active(&Id::ReplacePopup).is_ok());
}
/// Returns whether radio replace is in "extended" mode (for many files)
pub(super) fn is_radio_replace_extended(&self) -> bool {
self.app.mounted(&Id::ReplacingFilesListPopup)
}
pub(super) fn umount_radio_replace(&mut self) {
let _ = self.app.umount(&Id::ReplacePopup);
let _ = self.app.umount(&Id::ReplacingFilesListPopup); // NOTE: replace anyway
}
pub(super) fn mount_file_info(&mut self, file: &File) {

View File

@@ -99,29 +99,16 @@ impl SetupActivity {
Ok(State::One(StateValue::Usize(idx))) => Some(idx),
_ => None,
};
if let Some(idx) = idx {
let key: Option<String> = self.config().iter_ssh_keys().nth(idx).cloned();
if let Some(key) = key {
match self.config().get_ssh_key(&key) {
Ok(opt) => {
if let Some((host, username, _)) = opt {
if let Err(err) = self.delete_ssh_key(host.as_str(), username.as_str())
// get ssh key and delete it
if let Some(Err(err)) = idx
.and_then(|i| self.config().iter_ssh_keys().nth(i).cloned())
.and_then(|key| self.config().get_ssh_key(&key))
.map(|(host, username, _)| self.delete_ssh_key(host.as_str(), username.as_str()))
{
// Report error
self.mount_error(err.as_str());
}
}
}
Err(err) => {
// Report error
self.mount_error(
format!("Could not get ssh key \"{key}\": {err}").as_str(),
);
}
}
}
}
}
/// Create a new ssh key
pub(super) fn action_new_ssh_key(&mut self) -> Result<(), String> {

View File

@@ -77,16 +77,11 @@ impl SetupActivity {
Some(key) => {
// Get key path
match ctx.config().get_ssh_key(key) {
Ok(ssh_key) => match ssh_key {
None => Ok(()),
Some((_, _, key_path)) => {
match edit::edit_file(key_path.as_path()) {
Some((_, _, key_path)) => match edit::edit_file(key_path.as_path()) {
Ok(_) => Ok(()),
Err(err) => Err(format!("Could not edit ssh key: {err}")),
}
}
},
Err(err) => Err(format!("Could not read ssh key: {err}")),
}
}
None => Ok(()),

View File

@@ -126,7 +126,7 @@ impl SetupActivity {
.config()
.iter_ssh_keys()
.map(|x| {
let (addr, username, _) = self.config().get_ssh_key(x).ok().unwrap().unwrap();
let (addr, username, _) = self.config().get_ssh_key(x).unwrap();
format!("{username} at {addr}")
})
.collect();