15 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
veeso
05830db206 docs: User manual and get started links
Some checks failed
Install.sh / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Linux / build (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-09-16 10:36:17 +02:00
46 changed files with 1399 additions and 828 deletions

View File

@@ -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,7 +40,42 @@ jobs:
with:
toolchain: stable
targets: ${{ matrix.platform.target }}
- name: Install dependencies
- 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 \
@@ -66,18 +112,50 @@ jobs:
brew link --force openldap
brew link --force zlib
cpanm Parse::Yapp::Driver
- name: Build release
- 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: Prepare artifact files
- 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

@@ -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

@@ -44,19 +44,23 @@
## 0.19.0
Released on 20/09/2025
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.
- [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.
## 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).

597
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ name = "termscp"
readme = "README.md"
repository = "https://github.com/veeso/termscp"
version = "0.19.0"
rust-version = "1.87.0"
rust-version = "1.88.0"
[package.metadata.rpm]
package = "termscp"
@@ -71,11 +71,12 @@ 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.6"
tempfile = "3"
thiserror = "2"
tokio = { version = "1.44", features = ["rt"] }
tokio = { version = "1", features = ["rt"] }
toml = "^0.9"
tui-realm-stdlib = "3"
tuirealm = "3"

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Website</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Installation</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Installation</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">User manual</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">User manual</a>
</p>
<p align="center">
@@ -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.19.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"
@@ -191,7 +191,7 @@ Arch Linux users can install termscp from the official repositories.
pacman -S termscp
```
For more information or other platforms, please visit [termscp.veeso.dev](https://termscp.veeso.dev/#get-started) to view all installation methods.
For more information or other platforms, please visit [termscp.veeso.dev](https://termscp.veeso.dev/get-started.html) to view all installation methods.
⚠️ If you're looking on how to update termscp just run termscp from CLI with: `(sudo) termscp --update` ⚠️
@@ -237,7 +237,7 @@ You can make a donation with one of these platforms:
## User manual 📚
The user manual can be found on the [termscp's website](https://termscp.veeso.dev/#user-manual) or on [Github](docs/man.md).
The user manual can be found on the [termscp's website](https://termscp.veeso.dev/user-manual.html) or on [Github](docs/man.md).
---

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Webseite</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Installation</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Installation</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Benutzerhandbuch</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">Benutzerhandbuch</a>
</p>
<p align="center">
@@ -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.19.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"
@@ -186,7 +186,7 @@ Wenn Sie ein Windows-Benutzer sind, können Sie termscp mit [Chocolatey](https:/
choco install termscp
```
Für weitere Informationen oder andere Plattformen besuchen Sie bitte [termscp.veeso.dev](https://termscp.veeso.dev/termscp/#get-started), um alle Installationsmethoden anzuzeigen.
Für weitere Informationen oder andere Plattformen besuchen Sie bitte [termscp.veeso.dev](https://termscp.veeso.dev/termscp/get-started.html), um alle Installationsmethoden anzuzeigen.
⚠️ Wenn Sie wissen möchten, wie Sie termscp aktualisieren können, führen Sie einfach termscp über die CLI aus mit: `(sudo) termscp --update` ⚠️
@@ -234,7 +234,7 @@ Sie können mit einer dieser Plattformen spenden:
## User manual 📚
Das Benutzerhandbuch finden Sie auf der [termscp-Website](https://termscp.veeso.dev/termscp/#user-manual) oder auf [Github](man.md).
Das Benutzerhandbuch finden Sie auf der [termscp-Website](https://termscp.veeso.dev/termscp/user-manual.html) oder auf [Github](man.md).
---

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

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Sitio Web</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Instalación</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Instalación</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Manual de usuario</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">Manual de usuario</a>
</p>
<p align="center">
@@ -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.19.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"
@@ -186,7 +186,7 @@ mientras que si eres un usuario de Windows, puedes instalar termscp con [Chocola
choco install termscp
```
Para obtener más información u otras plataformas, visite [termscp.veeso.dev](https://termscp.veeso.dev/termscp/#get-started) para ver todos los métodos de instalación.
Para obtener más información u otras plataformas, visite [termscp.veeso.dev](https://termscp.veeso.dev/termscp/get-started.html) para ver todos los métodos de instalación.
⚠️ Si estás buscando cómo actualizar termscp, simplemente ejecute termscp desde CLI con:: `(sudo) termscp --update` ⚠️
@@ -232,7 +232,7 @@ Puedes hacer una donación con una de estas plataformas:
## Manual de usuario y documentación 📚
El manual del usuario se puede encontrar en el [sitio web de termscp](https://termscp.veeso.dev/termscp/#user-manual) o en [Github](man.md).
El manual del usuario se puede encontrar en el [sitio web de termscp](https://termscp.veeso.dev/termscp/user-manual.html) o en [Github](man.md).
---

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,9 +278,10 @@ 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 🥷
### Trabajar con múltiples archivos 🥷
Puedes optar por trabajar con varios archivos, usando estos controles:

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Site internet</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Installation</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Installation</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Manuel de l'Utilisateur</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">Manuel de l'Utilisateur</a>
</p>
<p align="center">
@@ -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.19.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"
@@ -186,7 +186,7 @@ tandis que si tu es un utilisateur Windows, tu peux installer termscp avec [Choc
choco install termscp
```
Pour plus d'informations sur les autres méthodes d'installation, veuillez visiter [termscp.veeso.dev](https://termscp.veeso.dev/termscp/#get-started).
Pour plus d'informations sur les autres méthodes d'installation, veuillez visiter [termscp.veeso.dev](https://termscp.veeso.dev/termscp/get-started.html).
⚠️ Si tu cherche comme de mettre à jour termscp, tu dois exécuter cette commande dans le terminal: `(sudo) termscp --update` ⚠️
@@ -232,7 +232,7 @@ Tu peux faire un don avec l'une de ces plateformes:
## Manuel d'utilisateur et Documentation 📚
Le manuel d'utilisateur peut être trouvé sur le [site de termscp](https://termscp.veeso.dev/termscp/#user-manual) ou sur [Github](man.md).
Le manuel d'utilisateur peut être trouvé sur le [site de termscp](https://termscp.veeso.dev/termscp/user-manual.html) ou sur [Github](man.md).
La documentation peut être trouvé sur Rust Docs <https://docs.rs/termscp>

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,9 +276,10 @@ 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 🥷
### Travailler sur plusieurs fichiers 🥷
Vous pouvez choisir de travailler sur plusieurs fichiers avec ces simples commandes :

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Sito</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Installazione</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Installazione</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Manuale utente</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">Manuale utente</a>
</p>
<p align="center">
@@ -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.19.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"
@@ -186,7 +186,7 @@ mentre se sei un utente Windows, puoi installare termscp con [Chocolatey](https:
choco install termscp
```
Per ulteriori informazioni sui metodi di installazione su altre piattaforme, visita [termscp.veeso.dev](https://termscp.veeso.dev/termscp/#get-started).
Per ulteriori informazioni sui metodi di installazione su altre piattaforme, visita [termscp.veeso.dev](https://termscp.veeso.dev/termscp/get-started.html).
⚠️ Se stavi cercando come aggiornare la tua versione di termscp, puoi semplicemente lanciare termscp con questi argomenti: `(sudo) termscp --update` ⚠️
@@ -232,7 +232,7 @@ Puoi fare una donazione tramite una di queste piattaforme:
## Manuale utente 📚
Il manuale utente lo puoi trovare sul [sito di termscp](https://termscp.veeso.dev/termscp/#user-manual) o su [Github](man.md).
Il manuale utente lo puoi trovare sul [sito di termscp](https://termscp.veeso.dev/termscp/user-manual.html) o su [Github](man.md).
---

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

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Website</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Instalação</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Instalação</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Manual do usuário</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">Manual do usuário</a>
</p>
<p align="center">
@@ -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.19.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"
@@ -195,7 +195,7 @@ Usuários do Arch Linux podem instalar o termscp pelos repositórios oficiais.
pacman -S termscp
```
Para mais informações ou outras plataformas, visite [termscp.veeso.dev](https://termscp.veeso.dev/#get-started) para ver todos os métodos de instalação.
Para mais informações ou outras plataformas, visite [termscp.veeso.dev](https://termscp.veeso.dev/get-started.html) para ver todos os métodos de instalação.
⚠️ Se você quer saber como atualizar o termscp, basta executar o termscp a partir do CLI com: `(sudo) termscp --update` ⚠️
@@ -241,7 +241,7 @@ Você pode fazer uma doação por meio de uma dessas plataformas:
## Manual do Usuário 📚
O manual do usuário pode ser encontrado no [site do termscp](https://termscp.veeso.dev/#user-manual) ou no [Github](docs/man.md).
O manual do usuário pode ser encontrado no [site do termscp](https://termscp.veeso.dev/user-manual.html) ou no [Github](docs/man.md).
---

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

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">网站</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">安装</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">安装</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">用户手册</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">用户手册</a>
</p>
<p align="center">
@@ -71,7 +71,7 @@
</p>
<p align="center"><a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
<p align="center">当前版本: 0.19.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"
@@ -189,7 +189,7 @@ curl -sSLf http://get-termscp.veeso.dev | sh
choco install termscp
```
如需更多信息或其他的平台支持,请访问 [termscp.veeso.dev](https://termscp.veeso.dev/termscp/#get-started) 查看所有安装方法。
如需更多信息或其他的平台支持,请访问 [termscp.veeso.dev](https://termscp.veeso.dev/termscp/get-started.html) 查看所有安装方法。
⚠️ 如果您正在寻找如何更新 termscp 只需从 CLI 运行 termscp `(sudo) termscp --update` ⚠️
@@ -238,7 +238,7 @@ choco install termscp
## 用户手册和文档 📚
用户手册可以在[termscp的网站](https://termscp.veeso.dev/termscp/#user-manual)或者在[Github](man.md)上找到。
用户手册可以在[termscp的网站](https://termscp.veeso.dev/termscp/user-manual.html)或者在[Github](man.md)上找到。
---

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

@@ -10,8 +10,8 @@
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() {
@@ -451,7 +451,7 @@ case $PLATFORM in
esac
completed "Congratulations! Termscp has successfully been installed on your system!"
info "If you're a new user, you might be interested in reading the user manual <https://termscp.veeso.dev/#user-manual>"
info "If you're a new user, you might be interested in reading the user manual <https://termscp.veeso.dev/user-manual.html>"
info "While if you've just updated your termscp version, you can find the changelog at this link <https://termscp.veeso.dev/#changelog>"
info "Remember that if you encounter any issue, you can report them on Github <https://github.com/veeso/termscp/issues/new>"
info "Feel free to open an issue also if you have an idea which could improve the project"

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,10 +65,10 @@ 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 {
e.fmt = Formatter::new(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

@@ -245,10 +245,10 @@ 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() {
opts = opts.password(password);
}
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(

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,10 +114,10 @@ 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) {
return Err(format!("Bad working directory argument: {err}"));
}
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

@@ -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;
Some((addr, username, rsa_key_path)) => {
let key_name: String = Self::make_mapkey(&addr, &username);
hosts.insert(key_name, rsa_key_path);
}
None => continue,
}
info!("Got SSH key for {}", key);
}

View File

@@ -30,13 +30,13 @@ 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) {
// Load parameters into components
match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
}
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),
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
}
}
}
@@ -99,13 +99,13 @@ 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) {
// Load parameters
match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
}
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),
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
}
}
}
@@ -129,10 +129,10 @@ 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() {
self.mount_error(format!("Could not write bookmarks: {err}").as_str());
}
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());
}
}

View File

@@ -126,13 +126,13 @@ 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) {
self.mount_port(
FormTab::HostBridge,
Self::get_default_port_for_protocol(remote_protocol),
);
}
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) => {

View File

@@ -687,30 +687,30 @@ 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) {
// make spans
let info_color = self.theme().misc_info_dialog;
assert!(
self.app
.remount(
Id::NewVersionChangelog,
Box::new(components::ReleaseNotes::new(release_notes, info_color)),
vec![]
)
.is_ok()
);
assert!(
self.app
.remount(
Id::InstallUpdatePopup,
Box::new(components::InstallUpdatePopup::new(info_color)),
vec![]
)
.is_ok()
);
assert!(self.app.active(&Id::InstallUpdatePopup).is_ok());
}
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!(
self.app
.remount(
Id::NewVersionChangelog,
Box::new(components::ReleaseNotes::new(release_notes, info_color)),
vec![]
)
.is_ok()
);
assert!(
self.app
.remount(
Id::InstallUpdatePopup,
Box::new(components::InstallUpdatePopup::new(info_color)),
vec![]
)
.is_ok()
);
assert!(self.app.active(&Id::InstallUpdatePopup).is_ok());
}
}

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(),
)
})
.map(|(x, _)| x)
.collect();
// Check whether to replace files
if !existing_files.is_empty()
&& !self.should_replace_files(existing_files)
{
return;
}
}
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
entries,
) = self.get_files_to_transfer_with_overwrites(
entries,
super::save::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(),
@@ -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(),
)
})
.map(|(x, _)| x)
.collect();
// Check whether to replace files
if !existing_files.is_empty()
&& !self.should_replace_files(existing_files)
{
return;
}
}
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
entries,
) = self.get_files_to_transfer_with_overwrites(
entries,
super::save::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(),

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) {
return;
}
}
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) {
return;
}
}
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
trace!(
"Asking user whether he wants to replace files {:?}",
file_names
);
if self.wait_for_pending_msg(&[
Msg::PendingAction(PendingActionMsg::CloseReplacePopups),
Msg::PendingAction(PendingActionMsg::TransferPendingFile),
]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile)
{
trace!("User wants to replace files");
/// 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!(
"User wants to replace all files, including file {}",
file.name()
);
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();
true
} else {
trace!("The user doesn't want replace file");
self.umount_radio_replace();
false
}
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))
}
}
_ => None,
}
}
}
#[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)
}
CmdResult::Submit(State::One(StateValue::Usize(1))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip))
}
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))
}
_ => 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,10 +510,10 @@ 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() {
error!("Failed to delete cache: {}", err);
}
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() {

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);
}
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,27 +99,14 @@ 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())
{
// 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(),
);
}
}
}
// 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());
}
}

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()) {
Ok(_) => Ok(()),
Err(err) => Err(format!("Could not edit ssh key: {err}")),
}
}
None => Ok(()),
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();