mirror of
https://github.com/veeso/termscp.git
synced 2025-12-06 17:15:35 -08:00
Compare commits
8 Commits
f4156a5059
...
0.19.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9b54a227b | ||
|
|
befc32198a | ||
|
|
7e5103ff7e | ||
|
|
2cb600083e | ||
|
|
47d23673e6 | ||
|
|
a0b357cf8c | ||
|
|
75943f2b93 | ||
|
|
085ab721f9 |
92
.github/workflows/build-artifacts.yml
vendored
92
.github/workflows/build-artifacts.yml
vendored
@@ -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-${{ 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
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -44,20 +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).
|
||||
|
||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -2032,7 +2032,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.57.0",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3022,9 +3022,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pavao"
|
||||
version = "0.2.13"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c62e3a544910e4a9bbb56218bad91593b79a27c676e54a36097200d10d2d6e"
|
||||
checksum = "f716d52d838ee3062b42302f328d6a3ecfb0006b651707886fa890e8951c7cf7"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"lazy_static",
|
||||
@@ -3036,9 +3036,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pavao-src"
|
||||
version = "4.22.0"
|
||||
version = "4.22.0-3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29b20a487b78fd44d081eb16717d062f78cae52888a7affeed1e7da3be1b9247"
|
||||
checksum = "18364b5a8ff85f60fcca22e0d21a60f50ab4b9152d51f7358f3b42bbace88aab"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"git2",
|
||||
@@ -3047,9 +3047,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pavao-sys"
|
||||
version = "0.2.13"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e72a8efe4193b073995a99981a288cfef121027d12c420c647c2f19d4103d4"
|
||||
checksum = "51792a86ac195a0e5b7a942f5b62e8f9494a260a5febe31673c5e7bb72ade55f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3553,9 +3553,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "remotefs-ssh"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29de4702886ae0e4433904d4f6f9b2ed2961d31d76a7e7bef58305097238eb6"
|
||||
checksum = "5ca8b65fbd60801ac03973a41196b030fb04d1ce1af33d3c58c5bad94d46eb27"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy-regex",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -316,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 🥷
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -251,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 |
|
||||
@@ -278,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:
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -249,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 |
|
||||
@@ -276,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 :
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -245,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 |
|
||||
@@ -272,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 🥷
|
||||
|
||||
@@ -278,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 🥷
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -278,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 🥷
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -245,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 |
|
||||
@@ -272,6 +272,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
|
||||
| `<CTRL+A>` | 选中所有文件 | |
|
||||
| `<ALT+A>` | 取消选择所有文件 | |
|
||||
| `<CTRL+C>` | 终止文件传输 | |
|
||||
| `<CTRL+S>` | 获取所选路径的总大小 | Size |
|
||||
| `<CTRL+T>` | 显示所有同步路径 | Track |
|
||||
|
||||
### 操作多个文件 🥷
|
||||
|
||||
94
src/ui/activities/filetransfer/actions/file_size.rs
Normal file
94
src/ui/activities/filetransfer/actions/file_size.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user