mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Compare commits
24 Commits
8715c2b6f9
...
v0.19.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32822037e | ||
|
|
abb5c212c5 | ||
|
|
e9b54a227b | ||
|
|
befc32198a | ||
|
|
7e5103ff7e | ||
|
|
2cb600083e | ||
|
|
47d23673e6 | ||
|
|
a0b357cf8c | ||
|
|
75943f2b93 | ||
|
|
085ab721f9 | ||
|
|
f4156a5059 | ||
|
|
4bebec369f | ||
|
|
05c8613279 | ||
|
|
205d2813ad | ||
|
|
86660a0cc9 | ||
|
|
05830db206 | ||
|
|
3c79e812eb | ||
|
|
0287e7706a | ||
|
|
67a14c2725 | ||
|
|
df03c5c1bf | ||
|
|
3ce3ffee3d | ||
|
|
c0b32a1847 | ||
|
|
81ae0035c3 | ||
|
|
783da22ca2 |
131
.github/workflows/build-artifacts.yml
vendored
131
.github/workflows/build-artifacts.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TERMSCP_VERSION: "0.17.0"
|
TERMSCP_VERSION: "0.19.0"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-binaries:
|
build-binaries:
|
||||||
@@ -14,13 +14,24 @@ jobs:
|
|||||||
platform:
|
platform:
|
||||||
- release_for: MacOS-x86_64
|
- release_for: MacOS-x86_64
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
|
platform: macos
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
script: macos.sh
|
|
||||||
|
|
||||||
- release_for: MacOS-M1
|
- release_for: MacOS-aarch64
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
|
platform: macos
|
||||||
target: aarch64-apple-darwin
|
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 }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
steps:
|
steps:
|
||||||
@@ -29,18 +40,122 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
targets: ${{ matrix.platform.target }}
|
targets: ${{ matrix.platform.target }}
|
||||||
- name: Build release
|
|
||||||
run: cargo build --release --target ${{ matrix.platform.target }}
|
- name: Install dependencies (Linux)
|
||||||
- name: Prepare artifact files
|
if: matrix.platform.platform == 'linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
make \
|
||||||
|
libgit2-dev \
|
||||||
|
build-essential \
|
||||||
|
pkg-config \
|
||||||
|
libbsd-dev \
|
||||||
|
libcap-dev \
|
||||||
|
libcups2-dev \
|
||||||
|
libgnutls28-dev \
|
||||||
|
libicu-dev \
|
||||||
|
libjansson-dev \
|
||||||
|
libkeyutils-dev \
|
||||||
|
libldap2-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libpam0g-dev \
|
||||||
|
libacl1-dev \
|
||||||
|
libarchive-dev \
|
||||||
|
flex \
|
||||||
|
bison \
|
||||||
|
libntirpc-dev \
|
||||||
|
libtracker-sparql-3.0-dev \
|
||||||
|
libglib2.0-dev \
|
||||||
|
libdbus-1-dev \
|
||||||
|
libsasl2-dev \
|
||||||
|
libunistring-dev \
|
||||||
|
libdbus-1-dev \
|
||||||
|
cpanminus;
|
||||||
|
sudo cpanm Parse::Yapp::Driver
|
||||||
|
|
||||||
|
- name: Install dependencies (MacOS)
|
||||||
|
if: matrix.platform.platform == 'macos'
|
||||||
|
run: |
|
||||||
|
brew update
|
||||||
|
brew install \
|
||||||
|
bison \
|
||||||
|
cpanminus \
|
||||||
|
cups \
|
||||||
|
flex \
|
||||||
|
gettext \
|
||||||
|
gmp \
|
||||||
|
gnutls \
|
||||||
|
icu4c \
|
||||||
|
jansson \
|
||||||
|
libarchive \
|
||||||
|
libbsd \
|
||||||
|
libunistring \
|
||||||
|
libgit2 \
|
||||||
|
libtirpc \
|
||||||
|
openldap \
|
||||||
|
pkg-config \
|
||||||
|
zlib
|
||||||
|
brew link --force bison
|
||||||
|
brew link --force cups
|
||||||
|
brew link --force flex
|
||||||
|
brew link --force gettext
|
||||||
|
brew link --force gmp
|
||||||
|
brew link --force gnutls
|
||||||
|
brew link --force icu4c
|
||||||
|
brew link --force jansson
|
||||||
|
brew link --force libarchive
|
||||||
|
brew link --force libbsd
|
||||||
|
brew link --force libgit2
|
||||||
|
brew link --force libtirpc
|
||||||
|
brew link --force libunistring
|
||||||
|
brew link --force openldap
|
||||||
|
brew link --force zlib
|
||||||
|
cpanm Parse::Yapp::Driver
|
||||||
|
- name: Build release (MacOS Intel)
|
||||||
|
if: matrix.platform.target == 'x86_64-apple-darwin'
|
||||||
|
run: cargo build --release --no-default-features --features keyring --target ${{ matrix.platform.target }}
|
||||||
|
|
||||||
|
- name: Build release (others)
|
||||||
|
if: matrix.platform.target != 'x86_64-apple-darwin'
|
||||||
|
run: cargo build --release --features smb-vendored --target ${{ matrix.platform.target }}
|
||||||
|
|
||||||
|
- name: Build deb
|
||||||
|
if: matrix.platform.platform == 'linux'
|
||||||
|
run: |
|
||||||
|
cargo install cargo-deb
|
||||||
|
cargo deb --target ${{ matrix.platform.target }} --features smb-vendored
|
||||||
|
|
||||||
|
- name: Prepare artifact files (Posix)
|
||||||
|
if: matrix.platform.platform != 'windows'
|
||||||
run: |
|
run: |
|
||||||
mkdir -p .artifact
|
mkdir -p .artifact
|
||||||
mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp
|
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
|
tar -czf .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz -C .artifact termscp
|
||||||
ls -l .artifact/
|
ls -l .artifact/
|
||||||
- name: "Upload artifact"
|
- name: Upload artifact (Posix)
|
||||||
|
if: matrix.platform.platform != 'windows'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
name: termscp-${{ matrix.platform.target }}
|
name: termscp-${{ matrix.platform.target }}
|
||||||
path: .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz
|
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
|
||||||
|
|||||||
7
.github/workflows/macos.yml
vendored
7
.github/workflows/macos.yml
vendored
@@ -22,6 +22,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
brew update
|
||||||
|
brew install \
|
||||||
|
pkg-config \
|
||||||
|
samba
|
||||||
|
brew link --force samba
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build
|
run: cargo build
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
|||||||
3
.github/workflows/website.yml
vendored
3
.github/workflows/website.yml
vendored
@@ -6,7 +6,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
paths:
|
paths:
|
||||||
- "./site/**/*"
|
- ".github/workflows/website.yml"
|
||||||
|
- "site/**"
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@@ -15,7 +15,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-2019
|
runs-on: windows-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -1,6 +1,8 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
- [Changelog](#changelog)
|
- [Changelog](#changelog)
|
||||||
|
- [0.19.0](#0190)
|
||||||
|
- [0.18.0](#0180)
|
||||||
- [0.17.0](#0170)
|
- [0.17.0](#0170)
|
||||||
- [0.16.1](#0161)
|
- [0.16.1](#0161)
|
||||||
- [0.16.0](#0160)
|
- [0.16.0](#0160)
|
||||||
@@ -40,6 +42,34 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 0.19.0
|
||||||
|
|
||||||
|
Released on 11/11/2025
|
||||||
|
|
||||||
|
- [Issue 297](https://github.com/veeso/termscp/issues/297): Added `<CTRL+S>` keybinding to get the total size of selected paths.
|
||||||
|
- [Issue 331](https://github.com/veeso/termscp/issues/331): Added new `import-ssh-hosts` CLI subcommand to import all the hosts from the ssh config as bookmarks.
|
||||||
|
- [Issue 335](https://github.com/veeso/termscp/issues/335): Changed file overwrite behaviour
|
||||||
|
- Now the user can choose for each file whether to overwrite, skip or overwrite all/skip all.
|
||||||
|
- [Issue 354](https://github.com/veeso/termscp/issues/354):
|
||||||
|
- Removed error popup message if failed to check for updates.
|
||||||
|
- Prevent long timeouts when checking for updates if the network is down or the DNS is not working.
|
||||||
|
- [Issue 356](https://github.com/veeso/termscp/issues/356): Fixed SSH auth issue not trying with the password if any RSA key was found.
|
||||||
|
- [Issue 334](https://github.com/veeso/termscp/issues/334): SMB support for MacOS with vendored build of libsmbclient.
|
||||||
|
- [Issue 337](https://github.com/veeso/termscp/issues/337): Migrated to libssh.org on Linux and MacOS for better ssh agent support.
|
||||||
|
- [Issue 361](https://github.com/veeso/termscp/issues/361): Report a message while calculating total size of files to transfer.
|
||||||
|
|
||||||
|
## 0.18.0
|
||||||
|
|
||||||
|
Released on 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).
|
||||||
|
- Command History
|
||||||
|
- Support for `cd` and `exit` commands as well.
|
||||||
|
- Exit just closes the terminal emulator.
|
||||||
|
- [Issue 345](https://github.com/veeso/termscp/issues/345): Default keys are used from `~/.ssh` directory if no keys are resolved for the host.
|
||||||
|
- **Updated dependencies** and updated the Rust edition to `2024`
|
||||||
|
|
||||||
## 0.17.0
|
## 0.17.0
|
||||||
|
|
||||||
Released on 24/03/2025
|
Released on 24/03/2025
|
||||||
|
|||||||
1968
Cargo.lock
generated
1968
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
36
Cargo.toml
@@ -10,8 +10,8 @@ license = "MIT"
|
|||||||
name = "termscp"
|
name = "termscp"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/veeso/termscp"
|
repository = "https://github.com/veeso/termscp"
|
||||||
version = "0.17.0"
|
version = "0.19.0"
|
||||||
rust-version = "1.85.0"
|
rust-version = "1.88.0"
|
||||||
|
|
||||||
[package.metadata.rpm]
|
[package.metadata.rpm]
|
||||||
package = "termscp"
|
package = "termscp"
|
||||||
@@ -60,6 +60,7 @@ regex = "^1"
|
|||||||
remotefs = "^0.3"
|
remotefs = "^0.3"
|
||||||
remotefs-aws-s3 = "0.4"
|
remotefs-aws-s3 = "0.4"
|
||||||
remotefs-kube = "0.4"
|
remotefs-kube = "0.4"
|
||||||
|
remotefs-smb = { version = "^0.3", optional = true }
|
||||||
remotefs-webdav = "^0.2"
|
remotefs-webdav = "^0.2"
|
||||||
rpassword = "^7"
|
rpassword = "^7"
|
||||||
self_update = { version = "^0.42", default-features = false, features = [
|
self_update = { version = "^0.42", default-features = false, features = [
|
||||||
@@ -70,30 +71,35 @@ self_update = { version = "^0.42", default-features = false, features = [
|
|||||||
"compression-zip-deflate",
|
"compression-zip-deflate",
|
||||||
] }
|
] }
|
||||||
serde = { version = "^1", features = ["derive"] }
|
serde = { version = "^1", features = ["derive"] }
|
||||||
|
shellexpand = "3"
|
||||||
simplelog = "^0.12"
|
simplelog = "^0.12"
|
||||||
ssh2-config = "^0.4"
|
ssh2-config = "^0.6"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1.44", features = ["rt"] }
|
tokio = { version = "1", features = ["rt"] }
|
||||||
toml = "^0.8"
|
toml = "^0.9"
|
||||||
tui-realm-stdlib = "2"
|
tui-realm-stdlib = "3"
|
||||||
tuirealm = "2"
|
tuirealm = "3"
|
||||||
|
tui-term = "0.2"
|
||||||
unicode-width = "^0.2"
|
unicode-width = "^0.2"
|
||||||
version-compare = "^0.2"
|
version-compare = "^0.2"
|
||||||
whoami = "^1.5"
|
whoami = "^1.6"
|
||||||
wildmatch = "^2"
|
wildmatch = "^2"
|
||||||
|
|
||||||
[target."cfg(not(target_os = \"macos\"))".dependencies]
|
|
||||||
remotefs-smb = { version = "^0.3", optional = true }
|
|
||||||
|
|
||||||
[target."cfg(target_family = \"unix\")".dependencies]
|
[target."cfg(target_family = \"unix\")".dependencies]
|
||||||
remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
|
remotefs-ftp = { version = "^0.3", features = [
|
||||||
remotefs-ssh = { version = "^0.6", features = ["ssh2-vendored"] }
|
"native-tls-vendored",
|
||||||
|
"native-tls",
|
||||||
|
] }
|
||||||
|
remotefs-ssh = { version = "^0.7", default-features = false, features = [
|
||||||
|
"find",
|
||||||
|
"libssh-vendored",
|
||||||
|
] }
|
||||||
uzers = "0.12"
|
uzers = "0.12"
|
||||||
|
|
||||||
[target."cfg(target_family = \"windows\")".dependencies]
|
[target."cfg(target_family = \"windows\")".dependencies]
|
||||||
remotefs-ftp = { version = "^0.2", features = ["native-tls"] }
|
remotefs-ftp = { version = "^0.3", features = ["native-tls"] }
|
||||||
remotefs-ssh = { version = "^0.6" }
|
remotefs-ssh = { version = "^0.7" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "^1"
|
pretty_assertions = "^1"
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -8,9 +8,9 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://termscp.veeso.dev" target="_blank">Website</a>
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Developed by <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Developed by <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Current version: 0.17.0 24/03/2025</p>
|
<p align="center">Current version: 0.19.0 11/11/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
@@ -144,6 +144,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
|
|||||||
- 📝 View and edit files with your favourite applications
|
- 📝 View and edit files with your favourite applications
|
||||||
- 💁 SFTP/SCP authentication with SSH keys and username/password
|
- 💁 SFTP/SCP authentication with SSH keys and username/password
|
||||||
- 🐧 Compatible with Windows, Linux, FreeBSD, NetBSD and MacOS
|
- 🐧 Compatible with Windows, Linux, FreeBSD, NetBSD and MacOS
|
||||||
|
- 🐚 Embedded terminal for executing commands on the system.
|
||||||
- 🎨 Make it yours!
|
- 🎨 Make it yours!
|
||||||
- Themes
|
- Themes
|
||||||
- Custom file explorer format
|
- Custom file explorer format
|
||||||
@@ -190,7 +191,7 @@ Arch Linux users can install termscp from the official repositories.
|
|||||||
pacman -S termscp
|
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` ⚠️
|
⚠️ If you're looking on how to update termscp just run termscp from CLI with: `(sudo) termscp --update` ⚠️
|
||||||
|
|
||||||
@@ -236,7 +237,7 @@ You can make a donation with one of these platforms:
|
|||||||
|
|
||||||
## User manual 📚
|
## 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
4
build.rs
4
build.rs
@@ -10,8 +10,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
posix: { target_family = "unix" },
|
posix: { target_family = "unix" },
|
||||||
win: { target_family = "windows" },
|
win: { target_family = "windows" },
|
||||||
// exclusive features
|
// exclusive features
|
||||||
smb: { all(feature = "smb", not( macos )) },
|
smb: { feature = "smb" },
|
||||||
smb_unix: { all(unix, feature = "smb", not(macos)) },
|
smb_unix: { all(unix, feature = "smb") },
|
||||||
smb_windows: { all(windows, feature = "smb") }
|
smb_windows: { all(windows, feature = "smb") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
dist/build/macos.sh
vendored
4
dist/build/macos.sh
vendored
@@ -81,7 +81,7 @@ fi
|
|||||||
# Build release (x86_64)
|
# Build release (x86_64)
|
||||||
X86_TARGET=""
|
X86_TARGET=""
|
||||||
X86_TARGET_DIR=""
|
X86_TARGET_DIR=""
|
||||||
if [ "$ARCH" = "aarch64" ]; then
|
if [ "$ARCH" = "x86_64" ]; then
|
||||||
X86_TARGET="--target x86_64-apple-darwin"
|
X86_TARGET="--target x86_64-apple-darwin"
|
||||||
X86_TARGET_DIR="target/x86_64-apple-darwin/release/"
|
X86_TARGET_DIR="target/x86_64-apple-darwin/release/"
|
||||||
fi
|
fi
|
||||||
@@ -92,7 +92,7 @@ RET_X86_64=$?
|
|||||||
|
|
||||||
ARM64_TARGET=""
|
ARM64_TARGET=""
|
||||||
ARM64_TARGET_DIR=""
|
ARM64_TARGET_DIR=""
|
||||||
if [ "$ARCH" = "aarch64" ]; then
|
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
||||||
ARM64_TARGET="--target aarch64-apple-darwin"
|
ARM64_TARGET="--target aarch64-apple-darwin"
|
||||||
ARM64_TARGET_DIR="target/aarch64-apple-darwin/release/"
|
ARM64_TARGET_DIR="target/aarch64-apple-darwin/release/"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://termscp.veeso.dev" target="_blank">Webseite</a>
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Entwickelt von <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Entwickelt von <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Aktuelle Version: 0.17.0 24/03/2025</p>
|
<p align="center">Aktuelle Version: 0.19.0 11/11/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<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
|
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` ⚠️
|
⚠️ 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 📚
|
## 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
- [Unterbefehle](#unterbefehle)
|
- [Unterbefehle](#unterbefehle)
|
||||||
- [Ein Thema importieren](#ein-thema-importieren)
|
- [Ein Thema importieren](#ein-thema-importieren)
|
||||||
- [Neueste Version installieren](#neueste-version-installieren)
|
- [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-Verbindungsparameter](#s3-verbindungsparameter)
|
||||||
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen-)
|
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen-)
|
||||||
- [Dateiexplorer 📂](#dateiexplorer-)
|
- [Dateiexplorer 📂](#dateiexplorer-)
|
||||||
@@ -29,9 +33,9 @@
|
|||||||
- [AWS S3 Adressargument](#aws-s3-adressargument-1)
|
- [AWS S3 Adressargument](#aws-s3-adressargument-1)
|
||||||
- [SMB Adressargument](#smb-adressargument-1)
|
- [SMB Adressargument](#smb-adressargument-1)
|
||||||
- [Wie das Passwort bereitgestellt werden kann 🔐](#wie-das-passwort-bereitgestellt-werden-kann--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)
|
- [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-Verbindungsparameter](#s3-verbindungsparameter-1)
|
||||||
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen--1)
|
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen--1)
|
||||||
- [Dateiexplorer 📂](#dateiexplorer--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
|
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
|
## S3-Verbindungsparameter
|
||||||
@@ -296,6 +316,7 @@ Diese Panels sind im Wesentlichen 3 (ja, tatsächlich drei):
|
|||||||
| <CTRL+A> | Alle Dateien auswählen | |
|
| <CTRL+A> | Alle Dateien auswählen | |
|
||||||
| <ALT+A> | Alle Dateien abwählen | |
|
| <ALT+A> | Alle Dateien abwählen | |
|
||||||
| <CTRL+C> | Dateiübertragungsvorgang abbrechen | |
|
| <CTRL+C> | Dateiübertragungsvorgang abbrechen | |
|
||||||
|
| `<CTRL+S>` | Gesamte Größe des ausgewählten Pfads abrufen | Size |
|
||||||
| <CTRL+T> | Alle synchronisierten Pfade anzeigen | Track |
|
| <CTRL+T> | Alle synchronisierten Pfade anzeigen | Track |
|
||||||
|
|
||||||
### Mit mehreren Dateien arbeiten 🥷
|
### Mit mehreren Dateien arbeiten 🥷
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://termscp.veeso.dev" target="_blank">Sitio Web</a>
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Desarrollado por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Desarrollado por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Versión actual: 0.17.0 24/03/2025</p>
|
<p align="center">Versión actual: 0.19.0 11/11/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<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
|
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` ⚠️
|
⚠️ 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 📚
|
## 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
- [Argumento de dirección de WebDAV](#argumento-de-dirección-de-webdav)
|
- [Argumento de dirección de WebDAV](#argumento-de-dirección-de-webdav)
|
||||||
- [Argumento dirección por SMB](#argumento-dirección-por-smb)
|
- [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-)
|
- [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)
|
- [S3 parámetros de conexión](#s3-parámetros-de-conexión)
|
||||||
- [Credenciales de S3 🦊](#credenciales-de-s3-)
|
- [Credenciales de S3 🦊](#credenciales-de-s3-)
|
||||||
- [Explorador de archivos 📂](#explorador-de-archivos-)
|
- [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`
|
- 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.
|
- 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
|
## 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 | |
|
| `<BACKTAB>` | Cambiar entre la pestaña de registro y el explorador | |
|
||||||
| `<A>` | Alternar archivos ocultos | All |
|
| `<A>` | Alternar archivos ocultos | All |
|
||||||
| `<B>` | Ordenar archivos por | Bubblesort? |
|
| `<B>` | Ordenar archivos por | Bubblesort? |
|
||||||
| `<C|F5>` | Copiar archivo / directorio | Copy |
|
| `<C\|F5>` | Copiar archivo / directorio | Copy |
|
||||||
| `<D|F7>` | Hacer directorio | Directory |
|
| `<D\|F7>` | Hacer directorio | Directory |
|
||||||
| `<E|F8|DEL>` | Eliminar archivo | Erase |
|
| `<E\|F8\|DEL>` | Eliminar archivo | Erase |
|
||||||
| `<F>` | Búsqueda de archivos | Find |
|
| `<F>` | Búsqueda de archivos | Find |
|
||||||
| `<G>` | Ir a la ruta proporcionada | Go to |
|
| `<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 |
|
| `<I>` | Mostrar información sobre el archivo | Info |
|
||||||
| `<K>` | Crear un enlace simbólico que apunte a la entrada seleccionada actualmente | symlinK |
|
| `<K>` | Crear un enlace simbólico que apunte a la entrada seleccionada actualmente | symlinK |
|
||||||
| `<L>` | Recargar contenido del directorio / Borrar selección | List |
|
| `<L>` | Recargar contenido del directorio / Borrar selección | List |
|
||||||
| `<M>` | Seleccione un archivo | Mark |
|
| `<M>` | Seleccione un archivo | Mark |
|
||||||
| `<N>` | Crear un nuevo archivo con el nombre proporcionado | New |
|
| `<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 |
|
| `<P>` | Open log panel | Panel |
|
||||||
| `<Q|F10>` | Salir de termscp | Quit |
|
| `<Q\|F10>` | Salir de termscp | Quit |
|
||||||
| `<R|F6>` | Renombrar archivo | Rename |
|
| `<R\|F6>` | Renombrar archivo | Rename |
|
||||||
| `<S|F2>` | Guardar archivo como... | Save |
|
| `<S\|F2>` | Guardar archivo como... | Save |
|
||||||
| `<T>` | Sincronizar los cambios en la ruta seleccionada con el control remoto | Track |
|
| `<T>` | Sincronizar los cambios en la ruta seleccionada con el control remoto | Track |
|
||||||
| `<U>` | Ir al directorio principal | Upper |
|
| `<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 |
|
| `<W>` | Abrir archivo con el programa proporcionado | With |
|
||||||
| `<X>` | Ejecutar un comando | eXecute |
|
| `<X>` | Ejecutar un comando | eXecute |
|
||||||
| `<Y>` | Alternar navegación sincronizada | sYnc |
|
| `<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 | |
|
| `<CTRL+A>` | Seleccionar todos los archivos | |
|
||||||
| `<ALT+A>` | Deseleccionar todos los archivos | |
|
| `<ALT+A>` | Deseleccionar todos los archivos | |
|
||||||
| `<CTRL+C>` | Abortar el proceso de transferencia de 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 |
|
| `<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:
|
Puedes optar por trabajar con varios archivos, usando estos controles:
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://termscp.veeso.dev" target="_blank">Site internet</a>
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Développé par <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Développé par <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Version actuelle: 0.17.0 24/03/2025</p>
|
<p align="center">Version actuelle: 0.19.0 11/11/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<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
|
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` ⚠️
|
⚠️ 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 📚
|
## 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>
|
La documentation peut être trouvé sur Rust Docs <https://docs.rs/termscp>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
- [Argument d'adresse WebDAV](#argument-dadresse-webdav)
|
- [Argument d'adresse WebDAV](#argument-dadresse-webdav)
|
||||||
- [Argument d'adresse SMB](#argument-dadresse-smb)
|
- [Argument d'adresse SMB](#argument-dadresse-smb)
|
||||||
- [Comment le mot de passe peut être fourni 🔐](#comment-le-mot-de-passe-peut-être-fourni-)
|
- [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)
|
- [S3 paramètres de connexion](#s3-paramètres-de-connexion)
|
||||||
- [Identifiants S3 🦊](#identifiants-s3-)
|
- [Identifiants S3 🦊](#identifiants-s3-)
|
||||||
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
|
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
|
||||||
@@ -142,7 +146,6 @@ syntaxe **Other systems**:
|
|||||||
smb://[username@]<server-name>[:port]/<share>[/path/.../]
|
smb://[username@]<server-name>[:port]/<share>[/path/.../]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Comment le mot de passe peut être fourni 🔐
|
#### 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.
|
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`
|
- 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.
|
- 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
|
## 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 | |
|
| `<BACKTAB>` | Basculer entre l'onglet journal et l'explorateur | |
|
||||||
| `<A>` | Basculer les fichiers cachés | All |
|
| `<A>` | Basculer les fichiers cachés | All |
|
||||||
| `<B>` | Trier les fichiers par | Bubblesort? |
|
| `<B>` | Trier les fichiers par | Bubblesort? |
|
||||||
| `<C|F5>` | Copier le fichier/répertoire | Copy |
|
| `<C\|F5>` | Copier le fichier/répertoire | Copy |
|
||||||
| `<D|F7>` | Créer un dossier | Directory |
|
| `<D\|F7>` | Créer un dossier | Directory |
|
||||||
| `<E|F8|DEL>` | Supprimer le fichier (Identique à `DEL`) | Erase |
|
| `<E\|F8\|DEL>` | Supprimer le fichier (Identique à `DEL`) | Erase |
|
||||||
| `<F>` | Rechercher des fichiers | Find |
|
| `<F>` | Rechercher des fichiers | Find |
|
||||||
| `<G>` | Aller au chemin fourni | Go to |
|
| `<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 |
|
| `<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 |
|
| `<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 |
|
| `<L>` | Recharger le contenu du répertoire actuel / Effacer la sélection | List |
|
||||||
| `<M>` | Sélectionner un fichier | Mark |
|
| `<M>` | Sélectionner un fichier | Mark |
|
||||||
| `<N>` | Créer un nouveau fichier avec le nom fourni | New |
|
| `<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 |
|
| `<P>` | Ouvre le panel de journals | Panel |
|
||||||
| `<Q|F10>` | Quitter termscp | Quit |
|
| `<Q\|F10>` | Quitter termscp | Quit |
|
||||||
| `<R|F6>` | Renommer le fichier | Rename |
|
| `<R\|F6>` | Renommer le fichier | Rename |
|
||||||
| `<S|F2>` | Enregistrer le fichier sous... | Save |
|
| `<S\|F2>` | Enregistrer le fichier sous... | Save |
|
||||||
| `<T>` | Synchroniser les modifications apportées au chemin sélectionné | Track |
|
| `<T>` | Synchroniser les modifications apportées au chemin sélectionné | Track |
|
||||||
| `<U>` | Aller dans le répertoire parent | Upper |
|
| `<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 |
|
| `<W>` | Ouvrir le fichier avec le programme spécifié | With |
|
||||||
| `<X>` | Exécuter une commande | eXecute |
|
| `<X>` | Exécuter une commande | eXecute |
|
||||||
| `<Y>` | Basculer la navigation synchronisée | sYnc |
|
| `<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 | |
|
| `<CTRL+A>` | Sélectionner tous les fichiers | |
|
||||||
| `<ALT+A>` | Desélectionner tous les fichiers | |
|
| `<ALT+A>` | Desélectionner tous les fichiers | |
|
||||||
| `<CTRL+C>` | Abandonner le processus de transfert de 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 |
|
| `<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 :
|
Vous pouvez choisir de travailler sur plusieurs fichiers avec ces simples commandes :
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://termscp.veeso.dev" target="_blank">Sito</a>
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Sviluppato da <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Sviluppato da <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Versione corrente: 0.17.0 24/03/2025</p>
|
<p align="center">Versione corrente: 0.19.0 11/11/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<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
|
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` ⚠️
|
⚠️ 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 📚
|
## 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
- [Argomento indirizzo per WebDAV](#argomento-indirizzo-per-webdav)
|
- [Argomento indirizzo per WebDAV](#argomento-indirizzo-per-webdav)
|
||||||
- [Indirizzo SMB](#indirizzo-smb)
|
- [Indirizzo SMB](#indirizzo-smb)
|
||||||
- [Come fornire la password 🔐](#come-fornire-la-password-)
|
- [Come fornire la password 🔐](#come-fornire-la-password-)
|
||||||
|
- [Sottocomandi](#sottocomandi)
|
||||||
|
- [Importare un tema](#importare-un-tema)
|
||||||
|
- [Installare l’ultima versione](#installare-lultima-versione)
|
||||||
|
- [Importare host SSH](#importare-host-ssh)
|
||||||
- [Parametri di connessione S3](#parametri-di-connessione-s3)
|
- [Parametri di connessione S3](#parametri-di-connessione-s3)
|
||||||
- [Credenziali S3 🦊](#credenziali-s3-)
|
- [Credenziali S3 🦊](#credenziali-s3-)
|
||||||
- [File explorer 📂](#file-explorer-)
|
- [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/.../]
|
smb://[username@]<server-name>[:port]/<share>[/path/.../]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Come fornire la password 🔐
|
#### Come fornire la password 🔐
|
||||||
|
|
||||||
Quando si usa l'argomento indirizzo non è possibile fornire la password direttamente nell'argomento, esistono però altri metodi per farlo:
|
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`
|
- 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).
|
- 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 l’ultima 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
|
## 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 | |
|
| `<BACKTAB>` | Cambia tra explorer e pannello di log | |
|
||||||
| `<A>` | Mostra/nascondi file nascosti | All |
|
| `<A>` | Mostra/nascondi file nascosti | All |
|
||||||
| `<B>` | Ordina file per | Bubblesort? |
|
| `<B>` | Ordina file per | Bubblesort? |
|
||||||
| `<C|F5>` | Copia file/directory | Copy |
|
| `<C\|F5>` | Copia file/directory | Copy |
|
||||||
| `<D|F7>` | Crea directory | Directory |
|
| `<D\|F7>` | Crea directory | Directory |
|
||||||
| `<E|F8|DEL>` | Elimina file | Erase |
|
| `<E\|F8\|DEL>` | Elimina file | Erase |
|
||||||
| `<F>` | Cerca file (wild match supportato) | Find |
|
| `<F>` | Cerca file (wild match supportato) | Find |
|
||||||
| `<G>` | Vai al percorso indicato | Go to |
|
| `<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 |
|
| `<I>` | Mostra informazioni per il file selezionato | Info |
|
||||||
| `<K>` | Crea un link simbolico che punta al file selezionato | symlinK |
|
| `<K>` | Crea un link simbolico che punta al file selezionato | symlinK |
|
||||||
| `<L>` | Ricarica posizione corrente / pulisci selezione file | List |
|
| `<L>` | Ricarica posizione corrente / pulisci selezione file | List |
|
||||||
| `<M>` | Seleziona file | Mark |
|
| `<M>` | Seleziona file | Mark |
|
||||||
| `<N>` | Crea nuovo file con il nome fornito | New |
|
| `<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 |
|
| `<P>` | Apri pannello log | Panel |
|
||||||
| `<Q|F10>` | Termina termscp | Quit |
|
| `<Q\|F10>` | Termina termscp | Quit |
|
||||||
| `<R|F6>` | Rinomina file | Rename |
|
| `<R\|F6>` | Rinomina file | Rename |
|
||||||
| `<S|F2>` | Salva file con nome | Save |
|
| `<S\|F2>` | Salva file con nome | Save |
|
||||||
| `<T>` | Sincronizza il percorso locale con l'host remoto | Track |
|
| `<T>` | Sincronizza il percorso locale con l'host remoto | Track |
|
||||||
| `<U>` | Vai alla directory padre | Upper |
|
| `<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 |
|
| `<W>` | Apri il file con il programma specificato | With |
|
||||||
| `<X>` | Esegui comando shell | eXecute |
|
| `<X>` | Esegui comando shell | eXecute |
|
||||||
| `<Y>` | Abilita/disabilita Sync-Browsing | sYnc |
|
| `<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 | |
|
| `<CTRL+A>` | Seleziona tutti i file | |
|
||||||
| `<ALT+A>` | Deseleziona tutti i file | |
|
| `<ALT+A>` | Deseleziona tutti i file | |
|
||||||
| `<CTRL+C>` | Annulla trasferimento file | |
|
| `<CTRL+C>` | Annulla trasferimento file | |
|
||||||
|
| `<CTRL+S>` | Ottieni la dimensione totale del percorso selezionato | Size |
|
||||||
| `<CTRL+T>` | Visualizza tutti i percorsi sincronizzati | Track |
|
| `<CTRL+T>` | Visualizza tutti i percorsi sincronizzati | Track |
|
||||||
|
|
||||||
### Lavora con più file 🥷
|
### Lavora con più file 🥷
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- [Subcommands](#subcommands)
|
- [Subcommands](#subcommands)
|
||||||
- [Import a theme](#import-a-theme)
|
- [Import a theme](#import-a-theme)
|
||||||
- [Install latest version](#install-latest-version)
|
- [Install latest version](#install-latest-version)
|
||||||
|
- [Import ssh hosts](#import-ssh-hosts)
|
||||||
- [S3 connection parameters](#s3-connection-parameters)
|
- [S3 connection parameters](#s3-connection-parameters)
|
||||||
- [S3 credentials 🦊](#s3-credentials-)
|
- [S3 credentials 🦊](#s3-credentials-)
|
||||||
- [File explorer 📂](#file-explorer-)
|
- [File explorer 📂](#file-explorer-)
|
||||||
@@ -166,6 +167,12 @@ Run termscp as `termscp theme <theme-file>`
|
|||||||
|
|
||||||
Run termscp as `termscp update`
|
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
|
## 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 | |
|
| `<CTRL+A>` | Select all files | |
|
||||||
| `<ALT+A>` | Deselect all files | |
|
| `<ALT+A>` | Deselect all files | |
|
||||||
| `<CTRL+C>` | Abort file transfer process | |
|
| `<CTRL+C>` | Abort file transfer process | |
|
||||||
|
| `<CTRL+S>` | Get total size of the selected path | Size |
|
||||||
| `<CTRL+T>` | Show all synchronized paths | Track |
|
| `<CTRL+T>` | Show all synchronized paths | Track |
|
||||||
|
|
||||||
### Work on multiple files 🥷
|
### Work on multiple files 🥷
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://termscp.veeso.dev" target="_blank">Website</a>
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Desenvolvido por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Desenvolvido por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Versão atual: 0.17.0 24/03/2025</p>
|
<p align="center">Versão atual: 0.19.0 11/11/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<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
|
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` ⚠️
|
⚠️ 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 📚
|
## 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- [Subcomandos](#subcomandos)
|
- [Subcomandos](#subcomandos)
|
||||||
- [Importar um Tema](#importar-um-tema)
|
- [Importar um Tema](#importar-um-tema)
|
||||||
- [Instalar a Última Versão](#instalar-a-última-versão)
|
- [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)
|
- [Parâmetros de Conexão do S3](#parâmetros-de-conexão-do-s3)
|
||||||
- [Credenciais do S3 🦊](#credenciais-do-s3-)
|
- [Credenciais do S3 🦊](#credenciais-do-s3-)
|
||||||
- [Explorador de Arquivos 📂](#explorador-de-arquivos-)
|
- [Explorador de Arquivos 📂](#explorador-de-arquivos-)
|
||||||
@@ -164,6 +165,12 @@ Execute o termscp como `termscp theme <theme-file>`
|
|||||||
|
|
||||||
Execute o termscp como `termscp update`
|
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
|
## 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 | |
|
| `<CTRL+A>` | Selecionar todos os arquivos | |
|
||||||
| `<ALT+A>` | Deselecionar todos os arquivos | |
|
| `<ALT+A>` | Deselecionar todos os arquivos | |
|
||||||
| `<CTRL+C>` | Abortir processo de transferência de arquivo | |
|
| `<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 |
|
| `<CTRL+T>` | Mostrar todos os caminhos sincronizados | Track |
|
||||||
|
|
||||||
### Trabalhar com múltiplos arquivos 🥷
|
### Trabalhar com múltiplos arquivos 🥷
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://termscp.veeso.dev" target="_blank">网站</a>
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">由 <a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
|
<p align="center">由 <a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
|
||||||
<p align="center">当前版本: 0.17.0 24/03/2025</p>
|
<p align="center">当前版本: 0.19.0 11/11/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
@@ -189,7 +189,7 @@ curl -sSLf http://get-termscp.veeso.dev | sh
|
|||||||
choco install termscp
|
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` ⚠️
|
⚠️ 如果您正在寻找如何更新 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)上找到。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
- [WebDAV 地址参数](#webdav-地址参数)
|
- [WebDAV 地址参数](#webdav-地址参数)
|
||||||
- [SMB 地址参数](#smb-地址参数)
|
- [SMB 地址参数](#smb-地址参数)
|
||||||
- [如何输入密码](#如何输入密码)
|
- [如何输入密码](#如何输入密码)
|
||||||
|
- [子命令](#子命令)
|
||||||
|
- [导入主题](#导入主题)
|
||||||
|
- [安装最新版本](#安装最新版本)
|
||||||
|
- [导入 SSH 主机](#导入-ssh-主机)
|
||||||
- [S3 连接参数](#s3-连接参数)
|
- [S3 连接参数](#s3-连接参数)
|
||||||
- [Aws S3 凭证](#aws-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`
|
- 通过 `sshpass`: 你可以通过 `sshpass` 传入密码, 例如: `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
|
||||||
- 提示输入密码:如果你不使用前面的任何方法,你会被提示输入密码,就像 `scp`、`ssh` 等比较经典的工具上一样。
|
- 提示输入密码:如果你不使用前面的任何方法,你会被提示输入密码,就像 `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 连接参数
|
## S3 连接参数
|
||||||
@@ -226,25 +245,25 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
|
|||||||
| `<BACKTAB>` | 在日志面板和管理器面板之间切换 | |
|
| `<BACKTAB>` | 在日志面板和管理器面板之间切换 | |
|
||||||
| `<A>` | 是否显示隐藏文件 | All |
|
| `<A>` | 是否显示隐藏文件 | All |
|
||||||
| `<B>` | 按..排序 | Bubblesort? |
|
| `<B>` | 按..排序 | Bubblesort? |
|
||||||
| `<C|F5>` | 复制文件(夹) | Copy |
|
| `<C\|F5>` | 复制文件(夹) | Copy |
|
||||||
| `<D|F7>` | 创建文件夹 | Directory |
|
| `<D\|F7>` | 创建文件夹 | Directory |
|
||||||
| `<E|F8|DEL>` | 删除文件 | Erase |
|
| `<E\|F8\|DEL>` | 删除文件 | Erase |
|
||||||
| `<F>` | 文件搜索 (支持通配符) | Find |
|
| `<F>` | 文件搜索 (支持通配符) | Find |
|
||||||
| `<G>` | 跳转到指定路径 | Go to |
|
| `<G>` | 跳转到指定路径 | Go to |
|
||||||
| `<H|F1>` | 显示帮助 | Help |
|
| `<H\|F1>` | 显示帮助 | Help |
|
||||||
| `<I>` | 显示选中文件(夹)信息 | Info |
|
| `<I>` | 显示选中文件(夹)信息 | Info |
|
||||||
| `<K>` | 创建指向当前选定条目的符号链接 | symlinK |
|
| `<K>` | 创建指向当前选定条目的符号链接 | symlinK |
|
||||||
| `<L>` | 刷新当前目录列表 / 清除选中状态 | List |
|
| `<L>` | 刷新当前目录列表 / 清除选中状态 | List |
|
||||||
| `<M>` | 选中文件 | Mark |
|
| `<M>` | 选中文件 | Mark |
|
||||||
| `<N>` | 使用键入的名称新建文件 | New |
|
| `<N>` | 使用键入的名称新建文件 | New |
|
||||||
| `<O|F4>` | 编辑文件;参考文本编辑器文档 | Open |
|
| `<O\|F4>` | 编辑文件;参考文本编辑器文档 | Open |
|
||||||
| `<P>` | 打开日志面板 | Panel |
|
| `<P>` | 打开日志面板 | Panel |
|
||||||
| `<Q|F10>` | 退出termscp | Quit |
|
| `<Q\|F10>` | 退出termscp | Quit |
|
||||||
| `<R|F7>` | 重命名文件 | Rename |
|
| `<R\|F7>` | 重命名文件 | Rename |
|
||||||
| `<S|F2>` | 另存为... | Save |
|
| `<S\|F2>` | 另存为... | Save |
|
||||||
| `<T>` | 显示所有同步路径 | Track |
|
| `<T>` | 显示所有同步路径 | Track |
|
||||||
| `<U>` | 进入上层目录 | Upper |
|
| `<U>` | 进入上层目录 | Upper |
|
||||||
| `<V|F3>` | 使用默认方式打开文件 | View |
|
| `<V\|F3>` | 使用默认方式打开文件 | View |
|
||||||
| `<W>` | 使用指定程序打开文件 | With |
|
| `<W>` | 使用指定程序打开文件 | With |
|
||||||
| `<X>` | 运行命令 | eXecute |
|
| `<X>` | 运行命令 | eXecute |
|
||||||
| `<Y>` | 是否开启同步浏览 | sYnc |
|
| `<Y>` | 是否开启同步浏览 | sYnc |
|
||||||
@@ -253,6 +272,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
|
|||||||
| `<CTRL+A>` | 选中所有文件 | |
|
| `<CTRL+A>` | 选中所有文件 | |
|
||||||
| `<ALT+A>` | 取消选择所有文件 | |
|
| `<ALT+A>` | 取消选择所有文件 | |
|
||||||
| `<CTRL+C>` | 终止文件传输 | |
|
| `<CTRL+C>` | 终止文件传输 | |
|
||||||
|
| `<CTRL+S>` | 获取所选路径的总大小 | Size |
|
||||||
| `<CTRL+T>` | 显示所有同步路径 | Track |
|
| `<CTRL+T>` | 显示所有同步路径 | Track |
|
||||||
|
|
||||||
### 操作多个文件 🥷
|
### 操作多个文件 🥷
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# -f, -y, --force, --yes
|
# -f, -y, --force, --yes
|
||||||
# Skip the confirmation prompt during installation
|
# Skip the confirmation prompt during installation
|
||||||
|
|
||||||
TERMSCP_VERSION="0.17.0"
|
TERMSCP_VERSION="0.19.0"
|
||||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
||||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
||||||
@@ -451,7 +451,7 @@ case $PLATFORM in
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
completed "Congratulations! Termscp has successfully been installed on your system!"
|
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 "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 "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"
|
info "Feel free to open an issue also if you have an idea which could improve the project"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
||||||
since last release, so if the latest version is not available yet,
|
since last release, so if the latest version is not available yet,
|
||||||
you can install it downloading the ZIP file from</span>
|
you can install it downloading the ZIP file from</span>
|
||||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.17.0.nupkg"
|
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.19.0.nupkg"
|
||||||
target="_blank">Github</a>
|
target="_blank">Github</a>
|
||||||
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
On Debian based distros, you can install termscp using the Deb
|
On Debian based distros, you can install termscp using the Deb
|
||||||
package via:
|
package via:
|
||||||
</p>
|
</p>
|
||||||
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.17.0_amd64.deb</span>
|
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.19.0_amd64.deb</span>
|
||||||
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
|
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
|
||||||
</div>
|
</div>
|
||||||
<h3>
|
<h3>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
<span translate="intro.versionAlert">termscp 0.17.0 is NOW out! Download it from</span>
|
<span translate="intro.versionAlert">termscp 0.19.0 is NOW out! Download it from</span>
|
||||||
<a href="/get-started.html" translate="intro.here">here!</a>
|
<a href="/get-started.html" translate="intro.here">here!</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "Get started →",
|
"getStarted": "Get started →",
|
||||||
"versionAlert": "termscp 0.17.0 is NOW out! Download it from",
|
"versionAlert": "termscp 0.19.0 is NOW out! Download it from",
|
||||||
"here": "here",
|
"here": "here",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
@@ -112,4 +112,4 @@
|
|||||||
"then": "Once started, you will be prompted whether to install or not the update. Confirm the installation and ta-dah, the new version of termscp should now be available on your machine"
|
"then": "Once started, you will be prompted whether to install or not the update. Confirm the installation and ta-dah, the new version of termscp should now be available on your machine"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "Para iniciar →",
|
"getStarted": "Para iniciar →",
|
||||||
"versionAlert": "termscp 0.17.0 ya está disponible! Descárgalo desde",
|
"versionAlert": "termscp 0.19.0 ya está disponible! Descárgalo desde",
|
||||||
"here": "aquì",
|
"here": "aquì",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
@@ -112,4 +112,4 @@
|
|||||||
"then": "Una vez iniciado, se le preguntará si desea instalar o no la actualización. Confirme la instalación y ta-dah, la nueva versión de termscp ahora debería estar disponible en su máquina"
|
"then": "Una vez iniciado, se le preguntará si desea instalar o no la actualización. Confirme la instalación y ta-dah, la nueva versión de termscp ahora debería estar disponible en su máquina"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "Pour commencer →",
|
"getStarted": "Pour commencer →",
|
||||||
"versionAlert": "termscp 0.17.0 est maintenant sorti! Télécharge-le depuis",
|
"versionAlert": "termscp 0.19.0 est maintenant sorti! Télécharge-le depuis",
|
||||||
"here": "ici",
|
"here": "ici",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
@@ -112,4 +112,4 @@
|
|||||||
"then": "Une fois démarré, vous serez invité à installer ou non la mise à jour. Confirmez l'installation et ta-dah, la nouvelle version de termscp devrait maintenant être disponible sur votre machine"
|
"then": "Une fois démarré, vous serez invité à installer ou non la mise à jour. Confirmez l'installation et ta-dah, la nouvelle version de termscp devrait maintenant être disponible sur votre machine"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
||||||
"getStarted": "Installa termscp →",
|
"getStarted": "Installa termscp →",
|
||||||
"versionAlert": "termscp 0.17.0 è ORA disponbile! Scaricalo da",
|
"versionAlert": "termscp 0.19.0 è ORA disponbile! Scaricalo da",
|
||||||
"here": "qui",
|
"here": "qui",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
@@ -112,4 +112,4 @@
|
|||||||
"then": "Una volta lanciato, se c'è un aggiornamento disponibile ti chiederà se procedere. Conferma e a questo punto dovrebbe installarlo. Se tutto è andato a buon fine, riavviando termscp dovrebbe essere l'ultima versione."
|
"then": "Una volta lanciato, se c'è un aggiornamento disponibile ti chiederà se procedere. Conferma e a questo punto dovrebbe installarlo. Se tutto è andato a buon fine, riavviando termscp dovrebbe essere l'ultima versione."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "开始 →",
|
"getStarted": "开始 →",
|
||||||
"versionAlert": "termscp 0.17.0 现已发布! 从下载",
|
"versionAlert": "termscp 0.19.0 现已发布! 从下载",
|
||||||
"here": "这里",
|
"here": "这里",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
@@ -112,4 +112,4 @@
|
|||||||
"then": "启动后,系统将提示您是否安装更新。 确认安装和 ta-dah,新版本的termscp 现在应该可以在你的机器上使用了"
|
"then": "启动后,系统将提示您是否安装更新。 确认安装和 ta-dah,新版本的termscp 现在应该可以在你的机器上使用了"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -448,35 +448,7 @@ impl ActivityManager {
|
|||||||
// -- misc
|
// -- misc
|
||||||
|
|
||||||
fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
|
fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
|
||||||
// Get config dir
|
crate::support::bookmarks_client(keyring)
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize configuration client
|
/// Initialize configuration client
|
||||||
|
|||||||
26
src/cli.rs
26
src/cli.rs
@@ -15,6 +15,9 @@ use crate::system::logging::LogLevel;
|
|||||||
|
|
||||||
pub enum Task {
|
pub enum Task {
|
||||||
Activity(NextActivity),
|
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),
|
ImportTheme(PathBuf),
|
||||||
InstallUpdate,
|
InstallUpdate,
|
||||||
Version,
|
Version,
|
||||||
@@ -72,7 +75,8 @@ pub struct Args {
|
|||||||
#[argh(subcommand)]
|
#[argh(subcommand)]
|
||||||
pub enum ArgsSubcommands {
|
pub enum ArgsSubcommands {
|
||||||
Config(ConfigArgs),
|
Config(ConfigArgs),
|
||||||
LoadTheme(LoadThemeArgs),
|
ImportSshHosts(ImportSshHostsArgs),
|
||||||
|
ImportTheme(ImportThemeArgs),
|
||||||
Update(UpdateArgs),
|
Update(UpdateArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,10 +90,20 @@ pub struct ConfigArgs {}
|
|||||||
#[argh(subcommand, name = "update")]
|
#[argh(subcommand, name = "update")]
|
||||||
pub struct UpdateArgs {}
|
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)]
|
#[derive(FromArgs)]
|
||||||
/// import the specified theme
|
/// import the specified theme
|
||||||
#[argh(subcommand, name = "theme")]
|
#[argh(subcommand, name = "theme")]
|
||||||
pub struct LoadThemeArgs {
|
pub struct ImportThemeArgs {
|
||||||
#[argh(positional)]
|
#[argh(positional)]
|
||||||
/// theme file
|
/// theme file
|
||||||
pub theme: PathBuf,
|
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 {
|
pub fn import_theme(theme: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
task: Task::ImportTheme(theme),
|
task: Task::ImportTheme(theme),
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ impl FileExplorerBuilder {
|
|||||||
|
|
||||||
/// Set formatter for FileExplorer
|
/// Set formatter for FileExplorer
|
||||||
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
|
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
|
||||||
if let Some(e) = self.explorer.as_mut() {
|
if let Some(e) = self.explorer.as_mut()
|
||||||
if let Some(fmt_str) = fmt_str {
|
&& let Some(fmt_str) = fmt_str
|
||||||
e.fmt = Formatter::new(fmt_str);
|
{
|
||||||
}
|
e.fmt = Formatter::new(fmt_str);
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -504,10 +504,7 @@ impl Formatter {
|
|||||||
};
|
};
|
||||||
// Match format length: group 3
|
// Match format length: group 3
|
||||||
let fmt_len: Option<usize> = match ®ex_match.get(3) {
|
let fmt_len: Option<usize> = match ®ex_match.get(3) {
|
||||||
Some(len) => match len.as_str().parse::<usize>() {
|
Some(len) => len.as_str().parse::<usize>().ok(),
|
||||||
Ok(len) => Some(len),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
// Match format extra: group 2 + 1
|
// Match format extra: group 2 + 1
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ pub struct FileExplorer {
|
|||||||
pub(crate) opts: ExplorerOpts,
|
pub(crate) opts: ExplorerOpts,
|
||||||
/// Formatter for file entries
|
/// Formatter for file entries
|
||||||
pub(crate) fmt: Formatter,
|
pub(crate) fmt: Formatter,
|
||||||
|
/// Is terminal open for this explorer?
|
||||||
|
terminal: bool,
|
||||||
/// Files in directory
|
/// Files in directory
|
||||||
files: Vec<File>,
|
files: Vec<File>,
|
||||||
/// files enqueued for transfer. Map between source and destination
|
/// files enqueued for transfer. Map between source and destination
|
||||||
@@ -73,6 +75,7 @@ impl Default for FileExplorer {
|
|||||||
opts: ExplorerOpts::empty(),
|
opts: ExplorerOpts::empty(),
|
||||||
fmt: Formatter::default(),
|
fmt: Formatter::default(),
|
||||||
files: Vec::new(),
|
files: Vec::new(),
|
||||||
|
terminal: false,
|
||||||
transfer_queue: HashMap::new(),
|
transfer_queue: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,6 +182,15 @@ impl FileExplorer {
|
|||||||
self.transfer_queue.clear();
|
self.transfer_queue.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle terminal state
|
||||||
|
pub fn toggle_terminal(&mut self, terminal: bool) {
|
||||||
|
self.terminal = terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn terminal_open(&self) -> bool {
|
||||||
|
self.terminal
|
||||||
|
}
|
||||||
|
|
||||||
// Formatting
|
// Formatting
|
||||||
|
|
||||||
/// Format a file entry
|
/// Format a file entry
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ impl HostBridgeParams {
|
|||||||
HostBridgeParams::Remote(_, params) => params,
|
HostBridgeParams::Remote(_, params) => params,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the host name for the bridge params
|
||||||
|
pub fn username(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
HostBridgeParams::Localhost(_) => Some(whoami::username()),
|
||||||
|
HostBridgeParams::Remote(_, params) => {
|
||||||
|
params.generic_params().and_then(|p| p.username.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Holds connection parameters for file transfers
|
/// Holds connection parameters for file transfers
|
||||||
@@ -42,6 +52,15 @@ pub struct FileTransferParams {
|
|||||||
pub local_path: Option<PathBuf>,
|
pub local_path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FileTransferParams {
|
||||||
|
/// Returns the remote path if set, otherwise returns the local path
|
||||||
|
pub fn username(&self) -> Option<String> {
|
||||||
|
self.params
|
||||||
|
.generic_params()
|
||||||
|
.and_then(|p| p.username.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Container for protocol params
|
/// Container for protocol params
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ProtocolParams {
|
pub enum ProtocolParams {
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ use remotefs_kube::KubeMultiPodFs as KubeFs;
|
|||||||
use remotefs_smb::SmbOptions;
|
use remotefs_smb::SmbOptions;
|
||||||
#[cfg(smb)]
|
#[cfg(smb)]
|
||||||
use remotefs_smb::{SmbCredentials, SmbFs};
|
use remotefs_smb::{SmbCredentials, SmbFs};
|
||||||
|
#[cfg(windows)]
|
||||||
|
use remotefs_ssh::LibSsh2Session as SshSession;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use remotefs_ssh::LibSshSession as SshSession;
|
||||||
use remotefs_ssh::{ScpFs, SftpFs, SshAgentIdentity, SshConfigParseRule, SshOpts};
|
use remotefs_ssh::{ScpFs, SftpFs, SshAgentIdentity, SshConfigParseRule, SshOpts};
|
||||||
use remotefs_webdav::WebDAVFs;
|
use remotefs_webdav::WebDAVFs;
|
||||||
|
|
||||||
@@ -138,12 +142,18 @@ impl RemoteFsBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build scp client
|
/// Build scp client
|
||||||
fn scp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> ScpFs {
|
fn scp_client(
|
||||||
|
params: GenericProtocolParams,
|
||||||
|
config_client: &ConfigClient,
|
||||||
|
) -> ScpFs<SshSession> {
|
||||||
Self::build_ssh_opts(params, config_client).into()
|
Self::build_ssh_opts(params, config_client).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build sftp client
|
/// Build sftp client
|
||||||
fn sftp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> SftpFs {
|
fn sftp_client(
|
||||||
|
params: GenericProtocolParams,
|
||||||
|
config_client: &ConfigClient,
|
||||||
|
) -> SftpFs<SshSession> {
|
||||||
Self::build_ssh_opts(params, config_client).into()
|
Self::build_ssh_opts(params, config_client).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +243,11 @@ impl RemoteFsBuilder {
|
|||||||
debug!("no username was provided, using current username");
|
debug!("no username was provided, using current username");
|
||||||
opts = opts.username(whoami::username());
|
opts = opts.username(whoami::username());
|
||||||
}
|
}
|
||||||
if let Some(password) = params.password {
|
// 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
|
||||||
|
&& !password.is_empty()
|
||||||
|
{
|
||||||
opts = opts.password(password);
|
opts = opts.password(password);
|
||||||
}
|
}
|
||||||
if let Some(config_path) = config_client.get_ssh_config() {
|
if let Some(config_path) = config_client.get_ssh_config() {
|
||||||
|
|||||||
27
src/main.rs
27
src/main.rs
@@ -22,7 +22,7 @@ extern crate log;
|
|||||||
extern crate magic_crypt;
|
extern crate magic_crypt;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use self::activity_manager::{ActivityManager, NextActivity};
|
use self::activity_manager::{ActivityManager, NextActivity};
|
||||||
@@ -72,7 +72,10 @@ fn main() -> MainResult<()> {
|
|||||||
fn parse_args(args: Args) -> Result<RunOpts, String> {
|
fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||||
let run_opts = match args.nested {
|
let run_opts = match args.nested {
|
||||||
Some(ArgsSubcommands::Update(_)) => RunOpts::update(),
|
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(),
|
Some(ArgsSubcommands::Config(_)) => RunOpts::config(),
|
||||||
None => {
|
None => {
|
||||||
let mut run_opts: RunOpts = RunOpts::default();
|
let mut run_opts: RunOpts = RunOpts::default();
|
||||||
@@ -111,10 +114,10 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Local directory
|
// Local directory
|
||||||
if let Some(localdir) = run_opts.remote.local_dir.as_deref() {
|
if let Some(localdir) = run_opts.remote.local_dir.as_deref()
|
||||||
if let Err(err) = env::set_current_dir(localdir) {
|
&& let Err(err) = env::set_current_dir(localdir)
|
||||||
return Err(format!("Bad working directory argument: {err}"));
|
{
|
||||||
}
|
return Err(format!("Bad working directory argument: {err}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
run_opts
|
run_opts
|
||||||
@@ -127,6 +130,7 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
|||||||
/// Run task and return rc
|
/// Run task and return rc
|
||||||
fn run(run_opts: RunOpts) -> MainResult<()> {
|
fn run(run_opts: RunOpts) -> MainResult<()> {
|
||||||
match run_opts.task {
|
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::ImportTheme(theme) => run_import_theme(&theme),
|
||||||
Task::InstallUpdate => run_install_update(),
|
Task::InstallUpdate => run_install_update(),
|
||||||
Task::Activity(activity) => {
|
Task::Activity(activity) => {
|
||||||
@@ -145,6 +149,17 @@ fn print_version() -> MainResult<()> {
|
|||||||
Ok(())
|
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<()> {
|
fn run_import_theme(theme: &Path) -> MainResult<()> {
|
||||||
match support::import_theme(theme) {
|
match support::import_theme(theme) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
//!
|
//!
|
||||||
//! this module exposes some extra run modes for termscp, meant to be used for "support", such as installing themes
|
//! 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::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
pub use self::import_ssh_hosts::import_ssh_hosts;
|
||||||
use crate::system::auto_update::{Update, UpdateStatus};
|
use crate::system::auto_update::{Update, UpdateStatus};
|
||||||
|
use crate::system::bookmarks_client::BookmarksClient;
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
use crate::system::notifications::Notification;
|
use crate::system::notifications::Notification;
|
||||||
@@ -79,10 +82,40 @@ fn get_config_client() -> Option<ConfigClient> {
|
|||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
Ok(dir) => {
|
Ok(dir) => {
|
||||||
let (cfg_path, ssh_key_dir) = environment::get_config_paths(dir.as_path());
|
let (cfg_path, ssh_key_dir) = environment::get_config_paths(dir.as_path());
|
||||||
match ConfigClient::new(cfg_path.as_path(), ssh_key_dir.as_path()) {
|
ConfigClient::new(cfg_path.as_path(), ssh_key_dir.as_path()).ok()
|
||||||
Err(_) => None,
|
|
||||||
Ok(c) => Some(c),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
326
src/support/import_ssh_hosts.rs
Normal file
326
src/support/import_ssh_hosts.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
//!
|
//!
|
||||||
//! Automatic update module. This module is used to upgrade the current version of termscp to the latest available on Github
|
//! Automatic update module. This module is used to upgrade the current version of termscp to the latest available on Github
|
||||||
|
|
||||||
|
use std::net::ToSocketAddrs as _;
|
||||||
|
|
||||||
use self_update::backends::github::Update as GithubUpdater;
|
use self_update::backends::github::Update as GithubUpdater;
|
||||||
pub use self_update::errors::Error as UpdateError;
|
pub use self_update::errors::Error as UpdateError;
|
||||||
use self_update::update::Release as UpdRelease;
|
use self_update::update::Release as UpdRelease;
|
||||||
@@ -67,6 +69,9 @@ impl Update {
|
|||||||
/// otherwise if no version is available, return None
|
/// otherwise if no version is available, return None
|
||||||
/// In case of error returns Error with the error description
|
/// In case of error returns Error with the error description
|
||||||
pub fn is_new_version_available() -> Result<Option<Release>, UpdateError> {
|
pub fn is_new_version_available() -> Result<Option<Release>, UpdateError> {
|
||||||
|
// check if api.github.com is reachable before doing anything
|
||||||
|
Self::check_github_api_reachable()?;
|
||||||
|
|
||||||
info!("Checking whether a new version is available...");
|
info!("Checking whether a new version is available...");
|
||||||
GithubUpdater::configure()
|
GithubUpdater::configure()
|
||||||
// Set default options
|
// Set default options
|
||||||
@@ -83,6 +88,27 @@ impl Update {
|
|||||||
.map(Self::check_version)
|
.map(Self::check_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if api.github.com is reachable
|
||||||
|
/// This is useful to avoid long timeouts when the network is down
|
||||||
|
/// or the DNS is not working
|
||||||
|
fn check_github_api_reachable() -> Result<(), UpdateError> {
|
||||||
|
let Some(socket_addr) = ("api.github.com", 443)
|
||||||
|
.to_socket_addrs()
|
||||||
|
.ok()
|
||||||
|
.and_then(|mut i| i.next())
|
||||||
|
else {
|
||||||
|
error!("Could not resolve api.github.com");
|
||||||
|
return Err(UpdateError::Network(
|
||||||
|
"Could not resolve api.github.com".into(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// just try to open a connection to api.github.com with a timeout of 5 seconds with tcp
|
||||||
|
std::net::TcpStream::connect_timeout(&socket_addr, std::time::Duration::from_secs(5))
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| UpdateError::Network(format!("Could not reach api.github.com: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
/// In case received version is newer than current one, version as Some is returned; otherwise None
|
/// In case received version is newer than current one, version as Some is returned; otherwise None
|
||||||
fn check_version(r: Release) -> Option<Release> {
|
fn check_version(r: Release) -> Option<Release> {
|
||||||
debug!("got version from GitHub: {}", r.version);
|
debug!("got version from GitHub: {}", r.version);
|
||||||
@@ -212,4 +238,9 @@ mod test {
|
|||||||
assert!(!Update::is_new_version_higher("0.9.9", "0.10.1"));
|
assert!(!Update::is_new_version_higher("0.9.9", "0.10.1"));
|
||||||
assert!(!Update::is_new_version_higher("0.10.9", "0.11.0"));
|
assert!(!Update::is_new_version_higher("0.10.9", "0.11.0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_check_whether_github_api_is_reachable() {
|
||||||
|
assert!(Update::check_github_api_reachable().is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,10 +153,7 @@ impl ConfigClient {
|
|||||||
// Convert string to `GroupDirs`
|
// Convert string to `GroupDirs`
|
||||||
match &self.config.user_interface.group_dirs {
|
match &self.config.user_interface.group_dirs {
|
||||||
None => None,
|
None => None,
|
||||||
Some(val) => match GroupDirs::from_str(val.as_str()) {
|
Some(val) => GroupDirs::from_str(val.as_str()).ok(),
|
||||||
Ok(val) => Some(val),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,19 +300,18 @@ impl ConfigClient {
|
|||||||
|
|
||||||
/// Get ssh key from host.
|
/// Get ssh key from host.
|
||||||
/// None is returned if key doesn't exist
|
/// 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) -> Option<SshHost> {
|
||||||
pub fn get_ssh_key(&self, mkey: &str) -> std::io::Result<Option<SshHost>> {
|
|
||||||
if self.degraded {
|
if self.degraded {
|
||||||
return Ok(None);
|
return None;
|
||||||
}
|
}
|
||||||
// Check if Key exists
|
// Check if Key exists
|
||||||
match self.config.remote.ssh_keys.get(mkey) {
|
match self.config.remote.ssh_keys.get(mkey) {
|
||||||
None => Ok(None),
|
None => None,
|
||||||
Some(key_path) => {
|
Some(key_path) => {
|
||||||
// Get host and username
|
// Get host and username
|
||||||
let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
|
let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
|
||||||
// Return key
|
// Return key
|
||||||
Ok(Some((host, username, PathBuf::from(key_path))))
|
Some((host, username, PathBuf::from(key_path)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,7 +450,7 @@ mod tests {
|
|||||||
// I/O
|
// I/O
|
||||||
assert!(client.add_ssh_key("Omar", "omar", "omar").is_err());
|
assert!(client.add_ssh_key("Omar", "omar", "omar").is_err());
|
||||||
assert!(client.del_ssh_key("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.write_config().is_err());
|
||||||
assert!(client.read_config().is_err());
|
assert!(client.read_config().is_err());
|
||||||
}
|
}
|
||||||
@@ -496,7 +492,7 @@ mod tests {
|
|||||||
let mut expected_key_path: PathBuf = key_path;
|
let mut expected_key_path: PathBuf = key_path;
|
||||||
expected_key_path.push("pi@192.168.1.31.key");
|
expected_key_path.push("pi@192.168.1.31.key");
|
||||||
assert_eq!(
|
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("192.168.1.31"),
|
||||||
String::from("pi"),
|
String::from("pi"),
|
||||||
@@ -687,7 +683,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
// Iterate keys
|
// Iterate keys
|
||||||
for key in client.iter_ssh_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.0, String::from("192.168.1.31"));
|
||||||
assert_eq!(host.1, String::from("pi"));
|
assert_eq!(host.1, String::from("pi"));
|
||||||
let mut expected_key_path: PathBuf = key_path.clone();
|
let mut expected_key_path: PathBuf = key_path.clone();
|
||||||
@@ -702,7 +698,7 @@ mod tests {
|
|||||||
assert_eq!(key, rsa_key);
|
assert_eq!(key, rsa_key);
|
||||||
}
|
}
|
||||||
// Unexisting key
|
// Unexisting key
|
||||||
assert!(client.get_ssh_key("test").ok().unwrap().is_none());
|
assert!(client.get_ssh_key("test").is_none());
|
||||||
// Delete key
|
// Delete key
|
||||||
assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok());
|
assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,15 +44,29 @@ impl SshKeyStorage {
|
|||||||
/// Resolve host via ssh2 configuration
|
/// Resolve host via ssh2 configuration
|
||||||
fn resolve_host_in_ssh2_configuration(&self, host: &str) -> Option<PathBuf> {
|
fn resolve_host_in_ssh2_configuration(&self, host: &str) -> Option<PathBuf> {
|
||||||
self.ssh_config.as_ref().and_then(|x| {
|
self.ssh_config.as_ref().and_then(|x| {
|
||||||
let key = x
|
x.query(host)
|
||||||
.query(host)
|
|
||||||
.identity_file
|
.identity_file
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|x| x.first().cloned());
|
.and_then(|x| x.first().cloned())
|
||||||
|
|
||||||
key
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get default SSH identity files that SSH would normally try
|
||||||
|
/// This mirrors the behavior of OpenSSH client
|
||||||
|
fn get_default_identity_files(&self) -> Vec<PathBuf> {
|
||||||
|
let Some(home_dir) = dirs::home_dir() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let ssh_dir = home_dir.join(".ssh");
|
||||||
|
|
||||||
|
// Standard SSH identity files in order of preference (matches OpenSSH)
|
||||||
|
["id_ed25519", "id_ecdsa", "id_rsa", "id_dsa"]
|
||||||
|
.iter()
|
||||||
|
.map(|key_name| ssh_dir.join(key_name))
|
||||||
|
.filter(|key_path| key_path.exists())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshKeyStorageTrait for SshKeyStorage {
|
impl SshKeyStorageTrait for SshKeyStorage {
|
||||||
@@ -66,9 +80,13 @@ impl SshKeyStorageTrait for SshKeyStorage {
|
|||||||
username, host
|
username, host
|
||||||
);
|
);
|
||||||
// otherwise search in configuration
|
// otherwise search in configuration
|
||||||
let key = self.resolve_host_in_ssh2_configuration(host)?;
|
if let Some(key) = self.resolve_host_in_ssh2_configuration(host) {
|
||||||
debug!("Found key in SSH config for {host}: {}", key.display());
|
debug!("Found key in SSH config for {host}: {}", key.display());
|
||||||
Some(key)
|
return Some(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// As a final fallback, try default SSH identity files (like regular ssh does)
|
||||||
|
self.get_default_identity_files().into_iter().next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,17 +103,11 @@ impl From<&ConfigClient> for SshKeyStorage {
|
|||||||
// Iterate over keys in storage
|
// Iterate over keys in storage
|
||||||
for key in cfg_client.iter_ssh_keys() {
|
for key in cfg_client.iter_ssh_keys() {
|
||||||
match cfg_client.get_ssh_key(key) {
|
match cfg_client.get_ssh_key(key) {
|
||||||
Ok(host) => match host {
|
Some((addr, username, rsa_key_path)) => {
|
||||||
Some((addr, username, rsa_key_path)) => {
|
let key_name: String = Self::make_mapkey(&addr, &username);
|
||||||
let key_name: String = Self::make_mapkey(&addr, &username);
|
hosts.insert(key_name, rsa_key_path);
|
||||||
hosts.insert(key_name, rsa_key_path);
|
|
||||||
}
|
|
||||||
None => continue,
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
error!("Failed to get SSH key for {}: {}", key, err);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
None => continue,
|
||||||
}
|
}
|
||||||
info!("Got SSH key for {}", key);
|
info!("Got SSH key for {}", key);
|
||||||
}
|
}
|
||||||
@@ -137,8 +149,14 @@ mod tests {
|
|||||||
*storage.resolve("192.168.1.31", "pi").unwrap(),
|
*storage.resolve("192.168.1.31", "pi").unwrap(),
|
||||||
exp_key_path
|
exp_key_path
|
||||||
);
|
);
|
||||||
// Verify unexisting key
|
// Verify key is a default key or none
|
||||||
assert!(storage.resolve("deskichup", "veeso").is_none());
|
let default_keys: Vec<PathBuf> = storage.get_default_identity_files().into_iter().collect();
|
||||||
|
|
||||||
|
if let Some(key) = storage.resolve("deskichup", "veeso") {
|
||||||
|
assert!(default_keys.contains(&key));
|
||||||
|
} else {
|
||||||
|
assert!(default_keys.is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ impl AuthActivity {
|
|||||||
pub(super) fn load_bookmark(&mut self, form_tab: FormTab, idx: usize) {
|
pub(super) fn load_bookmark(&mut self, form_tab: FormTab, idx: usize) {
|
||||||
if let Some(bookmarks_cli) = self.bookmarks_client() {
|
if let Some(bookmarks_cli) = self.bookmarks_client() {
|
||||||
// Iterate over bookmarks
|
// Iterate over bookmarks
|
||||||
if let Some(key) = self.bookmarks_list.get(idx) {
|
if let Some(key) = self.bookmarks_list.get(idx)
|
||||||
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
|
&& let Some(bookmark) = bookmarks_cli.get_bookmark(key)
|
||||||
// Load parameters into components
|
{
|
||||||
match form_tab {
|
// Load parameters into components
|
||||||
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
|
match form_tab {
|
||||||
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
|
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) {
|
pub(super) fn load_recent(&mut self, form_tab: FormTab, idx: usize) {
|
||||||
if let Some(client) = self.bookmarks_client() {
|
if let Some(client) = self.bookmarks_client() {
|
||||||
// Iterate over bookmarks
|
// Iterate over bookmarks
|
||||||
if let Some(key) = self.recents_list.get(idx) {
|
if let Some(key) = self.recents_list.get(idx)
|
||||||
if let Some(bookmark) = client.get_recent(key) {
|
&& let Some(bookmark) = client.get_recent(key)
|
||||||
// Load parameters
|
{
|
||||||
match form_tab {
|
// Load parameters
|
||||||
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
|
match form_tab {
|
||||||
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
|
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
|
/// Write bookmarks to file
|
||||||
fn write_bookmarks(&mut self) {
|
fn write_bookmarks(&mut self) {
|
||||||
if let Some(bookmarks_cli) = self.bookmarks_client() {
|
if let Some(bookmarks_cli) = self.bookmarks_client()
|
||||||
if let Err(err) = bookmarks_cli.write_bookmarks() {
|
&& let Err(err) = bookmarks_cli.write_bookmarks()
|
||||||
self.mount_error(format!("Could not write bookmarks: {err}").as_str());
|
{
|
||||||
}
|
self.mount_error(format!("Could not write bookmarks: {err}").as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ impl DeleteBookmarkPopup {
|
|||||||
.color(color)
|
.color(color)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.value(1)
|
.value(1)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
@@ -265,7 +265,7 @@ impl DeleteRecentPopup {
|
|||||||
.color(color)
|
.color(color)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.value(1)
|
.value(1)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
@@ -337,7 +337,7 @@ impl BookmarkSavePassword {
|
|||||||
.sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
|
.sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.value(0)
|
.value(0)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ impl RemoteProtocolRadio {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(if cfg!(smb) {
|
.choices(if cfg!(smb) {
|
||||||
&["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV", "SMB"]
|
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV", "SMB"].into_iter()
|
||||||
} else {
|
} else {
|
||||||
&["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"]
|
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"].into_iter()
|
||||||
})
|
})
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
@@ -126,7 +126,7 @@ impl HostBridgeProtocolRadio {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(if cfg!(smb) {
|
.choices(if cfg!(smb) {
|
||||||
&[
|
vec![
|
||||||
"Localhost",
|
"Localhost",
|
||||||
"SFTP",
|
"SFTP",
|
||||||
"SCP",
|
"SCP",
|
||||||
@@ -137,8 +137,9 @@ impl HostBridgeProtocolRadio {
|
|||||||
"WebDAV",
|
"WebDAV",
|
||||||
"SMB",
|
"SMB",
|
||||||
]
|
]
|
||||||
|
.into_iter()
|
||||||
} else {
|
} else {
|
||||||
&[
|
vec![
|
||||||
"Localhost",
|
"Localhost",
|
||||||
"SFTP",
|
"SFTP",
|
||||||
"SCP",
|
"SCP",
|
||||||
@@ -148,6 +149,7 @@ impl HostBridgeProtocolRadio {
|
|||||||
"Kube",
|
"Kube",
|
||||||
"WebDAV",
|
"WebDAV",
|
||||||
]
|
]
|
||||||
|
.into_iter()
|
||||||
})
|
})
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
@@ -649,7 +651,7 @@ impl RadioS3NewPathStyle {
|
|||||||
.color(color)
|
.color(color)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.title("New path style", Alignment::Left)
|
.title("New path style", Alignment::Left)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl ErrorPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text(&[TextSpan::from(text.as_ref())])
|
.text([TextSpan::from(text.as_ref())])
|
||||||
.wrap(true),
|
.wrap(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ impl InfoPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text(&[TextSpan::from(text.as_ref())])
|
.text([TextSpan::from(text.as_ref())])
|
||||||
.wrap(true),
|
.wrap(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ impl WaitPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text(&[TextSpan::from(text.as_ref())])
|
.text([TextSpan::from(text.as_ref())])
|
||||||
.wrap(true),
|
.wrap(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ impl WindowSizeError {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text(&[TextSpan::from(
|
.text([TextSpan::from(
|
||||||
"termscp requires at least 24 lines of height to run",
|
"termscp requires at least 24 lines of height to run",
|
||||||
)])
|
)])
|
||||||
.wrap(true),
|
.wrap(true),
|
||||||
@@ -163,7 +163,7 @@ impl QuitPopup {
|
|||||||
.foreground(color)
|
.foreground(color)
|
||||||
.title("Quit termscp?", Alignment::Center)
|
.title("Quit termscp?", Alignment::Center)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.choices(&["Yes", "No"]),
|
.choices(["Yes", "No"]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ impl InstallUpdatePopup {
|
|||||||
.foreground(color)
|
.foreground(color)
|
||||||
.title("Install update?", Alignment::Center)
|
.title("Install update?", Alignment::Center)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.choices(&["Yes", "No"]),
|
.choices(["Yes", "No"]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,13 +296,7 @@ impl ReleaseNotes {
|
|||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.title("Release notes", Alignment::Center)
|
.title("Release notes", Alignment::Center)
|
||||||
.text_rows(
|
.text_rows(notes.lines().map(TextSpan::from)),
|
||||||
notes
|
|
||||||
.lines()
|
|
||||||
.map(TextSpan::from)
|
|
||||||
.collect::<Vec<TextSpan>>()
|
|
||||||
.as_slice(),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ pub struct NewVersionDisclaimer {
|
|||||||
impl NewVersionDisclaimer {
|
impl NewVersionDisclaimer {
|
||||||
pub fn new(new_version: &str, color: Color) -> Self {
|
pub fn new(new_version: &str, color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Span::default().foreground(color).spans(&[
|
component: Span::default().foreground(color).spans([
|
||||||
TextSpan::from("termscp "),
|
TextSpan::from("termscp "),
|
||||||
TextSpan::new(new_version).underlined().bold(),
|
TextSpan::new(new_version).underlined().bold(),
|
||||||
TextSpan::from(
|
TextSpan::from(
|
||||||
@@ -91,7 +91,7 @@ pub struct HelpFooter {
|
|||||||
impl HelpFooter {
|
impl HelpFooter {
|
||||||
pub fn new(key_color: Color) -> Self {
|
pub fn new(key_color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Span::default().spans(&[
|
component: Span::default().spans([
|
||||||
TextSpan::from("<F1|CTRL+H>").bold().fg(key_color),
|
TextSpan::from("<F1|CTRL+H>").bold().fg(key_color),
|
||||||
TextSpan::from(" Help "),
|
TextSpan::from(" Help "),
|
||||||
TextSpan::from("<CTRL+C>").bold().fg(key_color),
|
TextSpan::from("<CTRL+C>").bold().fg(key_color),
|
||||||
|
|||||||
@@ -223,10 +223,7 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Report error
|
// Report error
|
||||||
error!("Failed to get latest version: {}", err);
|
error!("Failed to get latest version: {err}",);
|
||||||
self.mount_error(
|
|
||||||
format!("Could not check for new updates: {err}").as_str(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -126,13 +126,13 @@ impl AuthActivity {
|
|||||||
self.host_bridge_protocol = protocol;
|
self.host_bridge_protocol = protocol;
|
||||||
// Update port
|
// Update port
|
||||||
let port: u16 = self.get_input_port(FormTab::HostBridge);
|
let port: u16 = self.get_input_port(FormTab::HostBridge);
|
||||||
if let HostBridgeProtocol::Remote(remote_protocol) = protocol {
|
if let HostBridgeProtocol::Remote(remote_protocol) = protocol
|
||||||
if Self::is_port_standard(port) {
|
&& Self::is_port_standard(port)
|
||||||
self.mount_port(
|
{
|
||||||
FormTab::HostBridge,
|
self.mount_port(
|
||||||
Self::get_default_port_for_protocol(remote_protocol),
|
FormTab::HostBridge,
|
||||||
);
|
Self::get_default_port_for_protocol(remote_protocol),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FormMsg::RemoteProtocolChanged(protocol) => {
|
FormMsg::RemoteProtocolChanged(protocol) => {
|
||||||
|
|||||||
@@ -687,30 +687,30 @@ impl AuthActivity {
|
|||||||
|
|
||||||
/// mount release notes text area
|
/// mount release notes text area
|
||||||
pub(super) fn mount_release_notes(&mut self) {
|
pub(super) fn mount_release_notes(&mut self) {
|
||||||
if let Some(ctx) = self.context.as_ref() {
|
if let Some(ctx) = self.context.as_ref()
|
||||||
if let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES) {
|
&& let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES)
|
||||||
// make spans
|
{
|
||||||
let info_color = self.theme().misc_info_dialog;
|
// make spans
|
||||||
assert!(
|
let info_color = self.theme().misc_info_dialog;
|
||||||
self.app
|
assert!(
|
||||||
.remount(
|
self.app
|
||||||
Id::NewVersionChangelog,
|
.remount(
|
||||||
Box::new(components::ReleaseNotes::new(release_notes, info_color)),
|
Id::NewVersionChangelog,
|
||||||
vec![]
|
Box::new(components::ReleaseNotes::new(release_notes, info_color)),
|
||||||
)
|
vec![]
|
||||||
.is_ok()
|
)
|
||||||
);
|
.is_ok()
|
||||||
assert!(
|
);
|
||||||
self.app
|
assert!(
|
||||||
.remount(
|
self.app
|
||||||
Id::InstallUpdatePopup,
|
.remount(
|
||||||
Box::new(components::InstallUpdatePopup::new(info_color)),
|
Id::InstallUpdatePopup,
|
||||||
vec![]
|
Box::new(components::InstallUpdatePopup::new(info_color)),
|
||||||
)
|
vec![]
|
||||||
.is_ok()
|
)
|
||||||
);
|
.is_ok()
|
||||||
assert!(self.app.active(&Id::InstallUpdatePopup).is_ok());
|
);
|
||||||
}
|
assert!(self.app.active(&Id::InstallUpdatePopup).is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,41 +2,127 @@
|
|||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
|
/// Terminal command
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
enum Command {
|
||||||
|
Cd(String),
|
||||||
|
Exec(String),
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Command {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut parts = s.split_whitespace();
|
||||||
|
match parts.next() {
|
||||||
|
Some("cd") => {
|
||||||
|
if let Some(path) = parts.next() {
|
||||||
|
Ok(Command::Cd(path.to_string()))
|
||||||
|
} else {
|
||||||
|
Err("cd command requires a path".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("exit") | Some("logout") => Ok(Command::Exit),
|
||||||
|
Some(cmd) => Ok(Command::Exec(cmd.to_string())),
|
||||||
|
None => Err("".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_exec(&mut self, input: String) {
|
pub(crate) fn action_local_exec(&mut self, input: String) {
|
||||||
match self.host_bridge.exec(input.as_str()) {
|
self.action_exec(false, input);
|
||||||
Ok(output) => {
|
}
|
||||||
// Reload files
|
|
||||||
self.log(LogLevel::Info, format!("\"{input}\": {output}"));
|
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
||||||
}
|
self.action_exec(true, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_exec(&mut self, remote: bool, cmd: String) {
|
||||||
|
if cmd.is_empty() {
|
||||||
|
self.print_terminal("".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd = match Command::from_str(&cmd) {
|
||||||
|
Ok(cmd) => cmd,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Report err
|
self.log(LogLevel::Error, format!("Invalid command: {err}"));
|
||||||
self.log_and_alert(
|
self.print_terminal(err);
|
||||||
LogLevel::Error,
|
return;
|
||||||
format!("Could not execute command \"{input}\": {err}"),
|
}
|
||||||
);
|
};
|
||||||
|
|
||||||
|
match cmd {
|
||||||
|
Command::Cd(path) => {
|
||||||
|
self.action_exec_cd(remote, path);
|
||||||
|
}
|
||||||
|
Command::Exec(executable) => {
|
||||||
|
self.action_exec_executable(remote, executable);
|
||||||
|
}
|
||||||
|
Command::Exit => {
|
||||||
|
self.action_exec_exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
fn action_exec_exit(&mut self) {
|
||||||
match self.client.as_mut().exec(input.as_str()) {
|
self.browser.toggle_terminal(false);
|
||||||
Ok((rc, output)) => {
|
self.umount_exec();
|
||||||
// Reload files
|
}
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
fn action_exec_cd(&mut self, remote: bool, input: String) {
|
||||||
format!("\"{input}\" (exitcode: {rc}): {output}"),
|
let new_dir = if remote {
|
||||||
);
|
let dir_path: PathBuf =
|
||||||
|
self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||||
|
self.remote_changedir(dir_path.as_path(), true);
|
||||||
|
|
||||||
|
dir_path
|
||||||
|
} else {
|
||||||
|
let dir_path: PathBuf =
|
||||||
|
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||||
|
self.host_bridge_changedir(dir_path.as_path(), true);
|
||||||
|
|
||||||
|
dir_path
|
||||||
|
};
|
||||||
|
|
||||||
|
self.update_browser_file_list();
|
||||||
|
|
||||||
|
// update prompt and print the new directory
|
||||||
|
self.update_terminal_prompt();
|
||||||
|
self.print_terminal(new_dir.display().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a [`Command::Exec`] command
|
||||||
|
fn action_exec_executable(&mut self, remote: bool, cmd: String) {
|
||||||
|
let res = if remote {
|
||||||
|
self.client
|
||||||
|
.as_mut()
|
||||||
|
.exec(cmd.as_str())
|
||||||
|
.map(|(_, output)| output)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
self.host_bridge
|
||||||
|
.exec(cmd.as_str())
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(output) => {
|
||||||
|
self.print_terminal(output);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Report err
|
self.log(
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Could not execute command \"{input}\": {err}"),
|
format!("Could not execute command \"{cmd}\": {err}"),
|
||||||
);
|
);
|
||||||
|
self.print_terminal(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
// Iter files
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||||
if self.config().get_prompt_on_file_replace() {
|
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
||||||
// Check which file would be replaced
|
entries,
|
||||||
let existing_files: Vec<&File> = entries
|
) = self.get_files_to_transfer_with_overwrites(
|
||||||
.iter()
|
entries,
|
||||||
.filter(|(x, dest_path)| {
|
super::save::CheckFileExists::Remote,
|
||||||
self.remote_file_exists(
|
)
|
||||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
else {
|
||||||
)
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
})
|
return;
|
||||||
.map(|(x, _)| x)
|
};
|
||||||
.collect();
|
|
||||||
// Check whether to replace files
|
|
||||||
if !existing_files.is_empty()
|
|
||||||
&& !self.should_replace_files(existing_files)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = self.filetransfer_send(
|
if let Err(err) = self.filetransfer_send(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
@@ -131,24 +123,16 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
if self.config().get_prompt_on_file_replace() {
|
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
||||||
// Check which file would be replaced
|
entries,
|
||||||
let existing_files: Vec<&File> = entries
|
) = self.get_files_to_transfer_with_overwrites(
|
||||||
.iter()
|
entries,
|
||||||
.filter(|(x, dest_path)| {
|
super::save::CheckFileExists::HostBridge,
|
||||||
self.host_bridge_file_exists(
|
)
|
||||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
else {
|
||||||
)
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
})
|
return;
|
||||||
.map(|(x, _)| x)
|
};
|
||||||
.collect();
|
|
||||||
// Check whether to replace files
|
|
||||||
if !existing_files.is_empty()
|
|
||||||
&& !self.should_replace_files(existing_files)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = self.filetransfer_recv(
|
if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub(crate) mod copy;
|
|||||||
pub(crate) mod delete;
|
pub(crate) mod delete;
|
||||||
pub(crate) mod edit;
|
pub(crate) mod edit;
|
||||||
pub(crate) mod exec;
|
pub(crate) mod exec;
|
||||||
|
pub(crate) mod file_size;
|
||||||
pub(crate) mod filter;
|
pub(crate) mod filter;
|
||||||
pub(crate) mod find;
|
pub(crate) mod find;
|
||||||
pub(crate) mod mark;
|
pub(crate) mod mark;
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ impl FileTransferActivity {
|
|||||||
Ok(_) => self.log(LogLevel::Info, format!("Opened file `{}`", p.display())),
|
Ok(_) => self.log(LogLevel::Info, format!("Opened file `{}`", p.display())),
|
||||||
Err(err) => self.log(
|
Err(err) => self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Failed to open filoe `{}`: {}", p.display(), err),
|
format!("Failed to open file `{}`: {}", p.display(), err),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
// NOTE: clear screen in order to prevent crap on stderr
|
// NOTE: clear screen in order to prevent crap on stderr
|
||||||
|
|||||||
@@ -10,6 +10,37 @@ use super::{
|
|||||||
TransferPayload,
|
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 {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_saveas(&mut self, input: String) {
|
pub(crate) fn action_local_saveas(&mut self, input: String) {
|
||||||
self.local_send_file(TransferOpts::default().save_as(Some(input)));
|
self.local_send_file(TransferOpts::default().save_as(Some(input)));
|
||||||
@@ -60,22 +91,12 @@ impl FileTransferActivity {
|
|||||||
dest_path.push(save_as);
|
dest_path.push(save_as);
|
||||||
}
|
}
|
||||||
// Iter files
|
// Iter files
|
||||||
if self.config().get_prompt_on_file_replace() {
|
let TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
|
||||||
// Check which file would be replaced
|
self.get_files_to_transfer_with_overwrites(entries, CheckFileExists::Remote)
|
||||||
let existing_files: Vec<&File> = entries
|
else {
|
||||||
.iter()
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
.filter(|(x, dest_path)| {
|
return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = self.filetransfer_send(
|
if let Err(err) = self.filetransfer_send(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
@@ -128,23 +149,13 @@ impl FileTransferActivity {
|
|||||||
if let Some(save_as) = opts.save_as {
|
if let Some(save_as) = opts.save_as {
|
||||||
dest_path.push(save_as);
|
dest_path.push(save_as);
|
||||||
}
|
}
|
||||||
// Iter files
|
let TransferFilesWithOverwritesResult::FilesToTransfer(entries) = self
|
||||||
if self.config().get_prompt_on_file_replace() {
|
.get_files_to_transfer_with_overwrites(entries, CheckFileExists::HostBridge)
|
||||||
// Check which file would be replaced
|
else {
|
||||||
let existing_files: Vec<&File> = entries
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
.iter()
|
return;
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = self.filetransfer_recv(
|
if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
@@ -172,11 +183,17 @@ impl FileTransferActivity {
|
|||||||
self.mount_radio_replace(&file_name);
|
self.mount_radio_replace(&file_name);
|
||||||
// Wait for answer
|
// Wait for answer
|
||||||
trace!("Asking user whether he wants to replace file {}", file_name);
|
trace!("Asking user whether he wants to replace file {}", file_name);
|
||||||
if self.wait_for_pending_msg(&[
|
if matches!(
|
||||||
Msg::PendingAction(PendingActionMsg::CloseReplacePopups),
|
self.wait_for_pending_msg(&[
|
||||||
Msg::PendingAction(PendingActionMsg::TransferPendingFile),
|
Msg::PendingAction(PendingActionMsg::ReplaceCancel),
|
||||||
]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile)
|
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");
|
trace!("User wants to replace file");
|
||||||
self.umount_radio_replace();
|
self.umount_radio_replace();
|
||||||
true
|
true
|
||||||
@@ -187,28 +204,76 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set pending transfer for many files into storage and mount radio
|
/// Get files to replace
|
||||||
pub(crate) fn should_replace_files(&mut self, files: Vec<&File>) -> bool {
|
fn get_files_to_replace(&mut self, files: Vec<(File, PathBuf)>) -> GetFileToReplaceResult {
|
||||||
let file_names: Vec<String> = files.iter().map(|x| x.name()).collect();
|
// keep only files the user want to replace
|
||||||
self.mount_radio_replace_many(file_names.as_slice());
|
let mut files_to_replace = vec![];
|
||||||
// Wait for answer
|
let mut all_opts = AllOpts::Unset;
|
||||||
trace!(
|
for (file, p) in files {
|
||||||
"Asking user whether he wants to replace files {:?}",
|
// Check for all opts
|
||||||
file_names
|
match all_opts {
|
||||||
);
|
AllOpts::ReplaceAll => {
|
||||||
if self.wait_for_pending_msg(&[
|
trace!(
|
||||||
Msg::PendingAction(PendingActionMsg::CloseReplacePopups),
|
"User wants to replace all files, including file {}",
|
||||||
Msg::PendingAction(PendingActionMsg::TransferPendingFile),
|
file.name()
|
||||||
]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile)
|
);
|
||||||
{
|
files_to_replace.push((file, p));
|
||||||
trace!("User wants to replace files");
|
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();
|
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
|
/// Get file to check for path
|
||||||
@@ -224,4 +289,40 @@ impl FileTransferActivity {
|
|||||||
p.push(e.name());
|
p.push(e.name());
|
||||||
p
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,12 +53,20 @@ impl MockComponent for Log {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap_table()
|
.unwrap_table()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| ListItem::new(tui_realm_stdlib::utils::wrap_spans(row, width, &self.props)))
|
.map(|row| {
|
||||||
|
let row_refs = row.iter().collect::<Vec<_>>();
|
||||||
|
ListItem::new(tui_realm_stdlib::utils::wrap_spans(
|
||||||
|
row_refs.as_slice(),
|
||||||
|
width,
|
||||||
|
&self.props,
|
||||||
|
))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
let title = ("Log".to_string(), Alignment::Left);
|
||||||
let w = TuiList::new(list_items)
|
let w = TuiList::new(list_items)
|
||||||
.block(tui_realm_stdlib::utils::get_block(
|
.block(tui_realm_stdlib::utils::get_block(
|
||||||
borders,
|
borders,
|
||||||
Some(("Log".to_string(), Alignment::Left)),
|
Some(&title),
|
||||||
focus,
|
focus,
|
||||||
None,
|
None,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ pub struct FooterBar {
|
|||||||
impl FooterBar {
|
impl FooterBar {
|
||||||
pub fn new(key_color: Color) -> Self {
|
pub fn new(key_color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Span::default().spans(&[
|
component: Span::default().spans([
|
||||||
TextSpan::from("<F1|H>").bold().fg(key_color),
|
TextSpan::from("<F1|H>").bold().fg(key_color),
|
||||||
TextSpan::from(" Help "),
|
TextSpan::from(" Help "),
|
||||||
TextSpan::from("<TAB>").bold().fg(key_color),
|
TextSpan::from("<TAB>").bold().fg(key_color),
|
||||||
|
|||||||
@@ -13,21 +13,22 @@ mod log;
|
|||||||
mod misc;
|
mod misc;
|
||||||
mod popups;
|
mod popups;
|
||||||
mod selected_files;
|
mod selected_files;
|
||||||
|
mod terminal;
|
||||||
mod transfer;
|
mod transfer;
|
||||||
|
|
||||||
pub use misc::FooterBar;
|
pub use misc::FooterBar;
|
||||||
pub use popups::{
|
pub use popups::{
|
||||||
ATTR_FILES, ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup,
|
ATTR_FILES, ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, FatalPopup,
|
||||||
FatalPopup, FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
||||||
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
||||||
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
|
SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, SymlinkPopup,
|
||||||
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList,
|
SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList, WatcherPopup,
|
||||||
WatcherPopup,
|
|
||||||
};
|
};
|
||||||
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
|
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
|
||||||
|
|
||||||
pub use self::log::Log;
|
pub use self::log::Log;
|
||||||
pub use self::selected_files::SelectedFilesList;
|
pub use self::selected_files::SelectedFilesList;
|
||||||
|
pub use self::terminal::Terminal;
|
||||||
|
|
||||||
#[derive(Default, MockComponent)]
|
#[derive(Default, MockComponent)]
|
||||||
pub struct GlobalListener {
|
pub struct GlobalListener {
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ impl DeletePopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.value(1)
|
.value(1)
|
||||||
.title("Delete file(s)?", Alignment::Center),
|
.title("Delete file(s)?", Alignment::Center),
|
||||||
}
|
}
|
||||||
@@ -279,7 +279,7 @@ impl DisconnectPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.title("Are you sure you want to disconnect?", Alignment::Center),
|
.title("Are you sure you want to disconnect?", Alignment::Center),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,7 +344,7 @@ impl ErrorPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text(&[TextSpan::from(text.as_ref())])
|
.text([TextSpan::from(text.as_ref())])
|
||||||
.wrap(true),
|
.wrap(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,89 +362,6 @@ impl Component<Msg, NoUserEvent> for ErrorPopup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
|
||||||
pub struct ExecPopup {
|
|
||||||
component: Input,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExecPopup {
|
|
||||||
pub fn new(color: Color) -> Self {
|
|
||||||
Self {
|
|
||||||
component: Input::default()
|
|
||||||
.borders(
|
|
||||||
Borders::default()
|
|
||||||
.color(color)
|
|
||||||
.modifiers(BorderType::Rounded),
|
|
||||||
)
|
|
||||||
.foreground(color)
|
|
||||||
.input_type(InputType::Text)
|
|
||||||
.placeholder("ps a", Style::default().fg(Color::Rgb(128, 128, 128)))
|
|
||||||
.title("Execute command", Alignment::Center),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for ExecPopup {
|
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
|
||||||
match ev {
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Left, ..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::Move(Direction::Left));
|
|
||||||
Some(Msg::None)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Right, ..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::Move(Direction::Right));
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Delete, ..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::Cancel);
|
|
||||||
Some(Msg::None)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Backspace,
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::Delete);
|
|
||||||
Some(Msg::None)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Char(ch),
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::Type(ch));
|
|
||||||
Some(Msg::None)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Enter, ..
|
|
||||||
}) => match self.state() {
|
|
||||||
State::One(StateValue::String(i)) => {
|
|
||||||
Some(Msg::Transfer(TransferMsg::ExecuteCmd(i)))
|
|
||||||
}
|
|
||||||
_ => Some(Msg::None),
|
|
||||||
},
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
|
||||||
Some(Msg::Ui(UiMsg::CloseExecPopup))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(MockComponent)]
|
||||||
pub struct FatalPopup {
|
pub struct FatalPopup {
|
||||||
component: Paragraph,
|
component: Paragraph,
|
||||||
@@ -461,7 +378,7 @@ impl FatalPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text(&[TextSpan::from(text.as_ref())])
|
.text([TextSpan::from(text.as_ref())])
|
||||||
.wrap(true),
|
.wrap(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,6 +414,10 @@ impl FileInfoPopup {
|
|||||||
texts
|
texts
|
||||||
.add_col(TextSpan::from("Path: "))
|
.add_col(TextSpan::from("Path: "))
|
||||||
.add_col(TextSpan::new(path.as_str()).fg(Color::Yellow));
|
.add_col(TextSpan::new(path.as_str()).fg(Color::Yellow));
|
||||||
|
texts
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::from("Name: "))
|
||||||
|
.add_col(TextSpan::new(file.name()).fg(Color::Yellow));
|
||||||
if let Some(filetype) = file.extension() {
|
if let Some(filetype) = file.extension() {
|
||||||
texts
|
texts
|
||||||
.add_row()
|
.add_row()
|
||||||
@@ -723,6 +644,11 @@ impl KeybindingsPopup {
|
|||||||
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color))
|
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color))
|
||||||
.add_col(TextSpan::from(" Interrupt file transfer"))
|
.add_col(TextSpan::from(" Interrupt file transfer"))
|
||||||
.add_row()
|
.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::new("<CTRL+T>").bold().fg(key_color))
|
||||||
.add_col(TextSpan::from(" Show watched paths"))
|
.add_col(TextSpan::from(" Show watched paths"))
|
||||||
.build(),
|
.build(),
|
||||||
@@ -1121,7 +1047,7 @@ impl QuitPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.title("Are you sure you want to quit termscp?", Alignment::Center),
|
.title("Are you sure you want to quit termscp?", Alignment::Center),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1275,7 +1201,7 @@ impl ReplacePopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Replace", "Skip", "Replace All", "Skip All", "Cancel"])
|
||||||
.title(text, Alignment::Center),
|
.title(text, Alignment::Center),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1284,9 +1210,6 @@ impl ReplacePopup {
|
|||||||
impl Component<Msg, NoUserEvent> for ReplacePopup {
|
impl Component<Msg, NoUserEvent> for ReplacePopup {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
|
||||||
Some(Msg::Ui(UiMsg::ReplacePopupTabbed))
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Left, ..
|
code: Key::Left, ..
|
||||||
}) => {
|
}) => {
|
||||||
@@ -1300,102 +1223,36 @@ impl Component<Msg, NoUserEvent> for ReplacePopup {
|
|||||||
Some(Msg::None)
|
Some(Msg::None)
|
||||||
}
|
}
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
|
||||||
}
|
}
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('y'),
|
code: Key::Char('y'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::PendingAction(PendingActionMsg::TransferPendingFile)),
|
}) => Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('n'),
|
code: Key::Char('n'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups)),
|
}) => Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Enter, ..
|
code: Key::Enter, ..
|
||||||
}) => {
|
}) => match self.perform(Cmd::Submit) {
|
||||||
if matches!(
|
CmdResult::Submit(State::One(StateValue::Usize(0))) => {
|
||||||
self.perform(Cmd::Submit),
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite))
|
||||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
|
||||||
) {
|
|
||||||
Some(Msg::PendingAction(PendingActionMsg::TransferPendingFile))
|
|
||||||
} else {
|
|
||||||
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
|
|
||||||
}
|
}
|
||||||
}
|
CmdResult::Submit(State::One(StateValue::Usize(1))) => {
|
||||||
_ => None,
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip))
|
||||||
}
|
}
|
||||||
}
|
CmdResult::Submit(State::One(StateValue::Usize(2))) => {
|
||||||
}
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll))
|
||||||
|
}
|
||||||
#[derive(MockComponent)]
|
CmdResult::Submit(State::One(StateValue::Usize(3))) => {
|
||||||
pub struct ReplacingFilesListPopup {
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkipAll))
|
||||||
component: List,
|
}
|
||||||
}
|
CmdResult::Submit(State::One(StateValue::Usize(4))) => {
|
||||||
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
|
||||||
impl ReplacingFilesListPopup {
|
}
|
||||||
pub fn new(files: &[String], color: Color) -> Self {
|
_ => Some(Msg::None),
|
||||||
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)
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1502,7 +1359,7 @@ impl SortingPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(&["Name", "Modify time", "Creation time", "Size"])
|
.choices(["Name", "Modify time", "Creation time", "Size"])
|
||||||
.title("Sort files by…", Alignment::Center)
|
.title("Sort files by…", Alignment::Center)
|
||||||
.value(match value {
|
.value(match value {
|
||||||
FileSorting::CreationTime => 2,
|
FileSorting::CreationTime => 2,
|
||||||
@@ -1554,7 +1411,7 @@ impl StatusBarLocal {
|
|||||||
let file_sorting = file_sorting_label(browser.host_bridge().file_sorting);
|
let file_sorting = file_sorting_label(browser.host_bridge().file_sorting);
|
||||||
let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible());
|
let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible());
|
||||||
Self {
|
Self {
|
||||||
component: Span::default().spans(&[
|
component: Span::default().spans([
|
||||||
TextSpan::new("File sorting: ").fg(sorting_color),
|
TextSpan::new("File sorting: ").fg(sorting_color),
|
||||||
TextSpan::new(file_sorting).fg(sorting_color).reversed(),
|
TextSpan::new(file_sorting).fg(sorting_color).reversed(),
|
||||||
TextSpan::new(" Hidden files: ").fg(hidden_color),
|
TextSpan::new(" Hidden files: ").fg(hidden_color),
|
||||||
@@ -1589,7 +1446,7 @@ impl StatusBarRemote {
|
|||||||
false => "OFF",
|
false => "OFF",
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
component: Span::default().spans(&[
|
component: Span::default().spans([
|
||||||
TextSpan::new("File sorting: ").fg(sorting_color),
|
TextSpan::new("File sorting: ").fg(sorting_color),
|
||||||
TextSpan::new(file_sorting).fg(sorting_color).reversed(),
|
TextSpan::new(file_sorting).fg(sorting_color).reversed(),
|
||||||
TextSpan::new(" Hidden files: ").fg(hidden_color),
|
TextSpan::new(" Hidden files: ").fg(hidden_color),
|
||||||
@@ -1728,7 +1585,7 @@ impl SyncBrowsingMkdirPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.title(
|
.title(
|
||||||
format!(
|
format!(
|
||||||
r#"Sync browsing: directory "{dir_name}" doesn't exist. Do you want to create it?"#
|
r#"Sync browsing: directory "{dir_name}" doesn't exist. Do you want to create it?"#
|
||||||
@@ -1802,7 +1659,7 @@ impl WaitPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text(&[TextSpan::from(text.as_ref())])
|
.text([TextSpan::from(text.as_ref())])
|
||||||
.wrap(true),
|
.wrap(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1830,7 +1687,7 @@ impl WalkdirWaitPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text(&[
|
.text([
|
||||||
TextSpan::from(text.as_ref()),
|
TextSpan::from(text.as_ref()),
|
||||||
TextSpan::from("Press 'CTRL+C' to abort"),
|
TextSpan::from("Press 'CTRL+C' to abort"),
|
||||||
])
|
])
|
||||||
@@ -1961,7 +1818,7 @@ impl WatcherPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.title(text, Alignment::Center),
|
.title(text, Alignment::Center),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,21 +59,21 @@ impl ChmodPopup {
|
|||||||
},
|
},
|
||||||
user: Checkbox::default()
|
user: Checkbox::default()
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(&["Read", "Write", "Execute"])
|
.choices(["Read", "Write", "Execute"])
|
||||||
.title("User", Alignment::Left)
|
.title("User", Alignment::Left)
|
||||||
.borders(Borders::default().sides(BorderSides::NONE))
|
.borders(Borders::default().sides(BorderSides::NONE))
|
||||||
.values(&make_pex_values(pex.user()))
|
.values(&make_pex_values(pex.user()))
|
||||||
.rewind(true),
|
.rewind(true),
|
||||||
group: Checkbox::default()
|
group: Checkbox::default()
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(&["Read", "Write", "Execute"])
|
.choices(["Read", "Write", "Execute"])
|
||||||
.title("Group", Alignment::Left)
|
.title("Group", Alignment::Left)
|
||||||
.borders(Borders::default().sides(BorderSides::NONE))
|
.borders(Borders::default().sides(BorderSides::NONE))
|
||||||
.values(&make_pex_values(pex.group()))
|
.values(&make_pex_values(pex.group()))
|
||||||
.rewind(true),
|
.rewind(true),
|
||||||
others: Checkbox::default()
|
others: Checkbox::default()
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(&["Read", "Write", "Execute"])
|
.choices(["Read", "Write", "Execute"])
|
||||||
.title("Others", Alignment::Left)
|
.title("Others", Alignment::Left)
|
||||||
.borders(Borders::default().sides(BorderSides::NONE))
|
.borders(Borders::default().sides(BorderSides::NONE))
|
||||||
.values(&make_pex_values(pex.others()))
|
.values(&make_pex_values(pex.others()))
|
||||||
@@ -208,9 +208,11 @@ impl MockComponent for ChmodPopup {
|
|||||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
||||||
.unwrap_flag();
|
.unwrap_flag();
|
||||||
|
|
||||||
|
let div_title = (self.title.clone(), Alignment::Center);
|
||||||
|
|
||||||
let div = tui_realm_stdlib::utils::get_block(
|
let div = tui_realm_stdlib::utils::get_block(
|
||||||
Borders::default().color(self.color),
|
Borders::default().color(self.color),
|
||||||
Some((self.title.clone(), Alignment::Center)),
|
Some(&div_title),
|
||||||
focus,
|
focus,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use tui_realm_stdlib::Input;
|
use tui_realm_stdlib::Input;
|
||||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
@@ -158,7 +158,7 @@ impl OwnStates {
|
|||||||
.unwrap_or_else(|| PathBuf::from("/"));
|
.unwrap_or_else(|| PathBuf::from("/"));
|
||||||
|
|
||||||
// if path is `.`, then return None
|
// if path is `.`, then return None
|
||||||
if parent == PathBuf::from(".") {
|
if parent == Path::new(".") {
|
||||||
return Suggestion::None;
|
return Suggestion::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
136
src/ui/activities/filetransfer/components/terminal.rs
Normal file
136
src/ui/activities/filetransfer/components/terminal.rs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
mod component;
|
||||||
|
mod history;
|
||||||
|
mod line;
|
||||||
|
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::Color;
|
||||||
|
use tuirealm::{AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent};
|
||||||
|
|
||||||
|
use self::component::TerminalComponent;
|
||||||
|
use self::line::Line;
|
||||||
|
use super::Msg;
|
||||||
|
use crate::ui::activities::filetransfer::{TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent, Default)]
|
||||||
|
pub struct Terminal {
|
||||||
|
component: TerminalComponent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal {
|
||||||
|
/// Construct a new [`Terminal`] component with the given prompt line.
|
||||||
|
pub fn prompt(mut self, prompt: impl ToString) -> Self {
|
||||||
|
self.component = self.component.prompt(prompt);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a new [`Terminal`] component with the given title.
|
||||||
|
pub fn title(mut self, title: impl ToString) -> Self {
|
||||||
|
self.component
|
||||||
|
.attr(Attribute::Title, AttrValue::String(title.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_color(mut self, color: Color) -> Self {
|
||||||
|
self.component
|
||||||
|
.attr(Attribute::Borders, AttrValue::Color(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a new [`Terminal`] component with the foreground color
|
||||||
|
pub fn foreground(mut self, color: Color) -> Self {
|
||||||
|
self.component
|
||||||
|
.attr(Attribute::Foreground, AttrValue::Color(color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for Terminal {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseExecPopup))
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.component.perform(Cmd::Submit) {
|
||||||
|
CmdResult::Submit(state) => {
|
||||||
|
let cmd = state.unwrap_one().unwrap_string();
|
||||||
|
Some(Msg::Transfer(TransferMsg::ExecuteCmd(cmd)))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.component.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Cancel);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Delete, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Delete);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
|
self.component.perform(Cmd::Move(Direction::Up));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Down, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Move(Direction::Down));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Insert, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Toggle);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::PageDown,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Scroll(Direction::Down));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::PageUp, ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Scroll(Direction::Up));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(c), ..
|
||||||
|
}) => {
|
||||||
|
self.component.perform(Cmd::Type(c));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
289
src/ui/activities/filetransfer/components/terminal/component.rs
Normal file
289
src/ui/activities/filetransfer/components/terminal/component.rs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
use tui_term::vt100::Parser;
|
||||||
|
use tui_term::widget::PseudoTerminal;
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
|
use tuirealm::props::{BorderSides, BorderType, Style};
|
||||||
|
use tuirealm::ratatui::layout::Rect;
|
||||||
|
use tuirealm::ratatui::widgets::Block;
|
||||||
|
use tuirealm::{AttrValue, Attribute, MockComponent, Props, State, StateValue};
|
||||||
|
|
||||||
|
use super::Line;
|
||||||
|
use super::history::History;
|
||||||
|
|
||||||
|
const DEFAULT_HISTORY_SIZE: usize = 128;
|
||||||
|
|
||||||
|
pub struct TerminalComponent {
|
||||||
|
pub parser: Parser,
|
||||||
|
history: History,
|
||||||
|
line: Line,
|
||||||
|
props: Props,
|
||||||
|
scroll: usize,
|
||||||
|
size: (u16, u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TerminalComponent {
|
||||||
|
fn default() -> Self {
|
||||||
|
let props = Props::default();
|
||||||
|
let parser = Parser::new(40, 220, 2048);
|
||||||
|
|
||||||
|
TerminalComponent {
|
||||||
|
parser,
|
||||||
|
history: History::new(DEFAULT_HISTORY_SIZE),
|
||||||
|
line: Line::default(),
|
||||||
|
props,
|
||||||
|
scroll: 0,
|
||||||
|
size: (40, 220),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalComponent {
|
||||||
|
/// Set prompt line for the terminal
|
||||||
|
pub fn prompt(mut self, prompt: impl ToString) -> Self {
|
||||||
|
self.attr(Attribute::Content, AttrValue::String(prompt.to_string()));
|
||||||
|
self.write_prompt();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_prompt(&mut self) {
|
||||||
|
if let Some(value) = self.query(Attribute::Content) {
|
||||||
|
let prompt = value.unwrap_string();
|
||||||
|
self.parser.process(prompt.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set current line to the previous command in the [`History`]
|
||||||
|
fn history_prev(&mut self) {
|
||||||
|
if let Some(cmd) = self.history.previous() {
|
||||||
|
self.write_line(cmd.as_bytes());
|
||||||
|
self.line.set(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set current line to the next command in the [`History`]
|
||||||
|
fn history_next(&mut self) {
|
||||||
|
if let Some(cmd) = self.history.next() {
|
||||||
|
self.write_line(cmd.as_bytes());
|
||||||
|
self.line.set(cmd);
|
||||||
|
} else {
|
||||||
|
// If there is no next command, clear the line
|
||||||
|
self.line.set(String::new());
|
||||||
|
self.write_line(&[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a line to the terminal, processing it through the parser
|
||||||
|
fn write_line(&mut self, data: &[u8]) {
|
||||||
|
self.parser.process(b"\r");
|
||||||
|
// blank the line
|
||||||
|
self.write_prompt();
|
||||||
|
self.parser.process(&[b' '; 15]);
|
||||||
|
self.parser.process(b"\r");
|
||||||
|
self.write_prompt();
|
||||||
|
self.parser.process(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockComponent for TerminalComponent {
|
||||||
|
fn attr(&mut self, attr: tuirealm::Attribute, value: AttrValue) {
|
||||||
|
if attr == Attribute::Text {
|
||||||
|
if let tuirealm::AttrValue::String(s) = value {
|
||||||
|
self.parser.process(b"\r");
|
||||||
|
self.parser.process(s.as_bytes());
|
||||||
|
self.parser.process(b"\r");
|
||||||
|
self.write_prompt();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.props.set(attr, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||||
|
match cmd {
|
||||||
|
Cmd::Type(s) => {
|
||||||
|
if !s.is_ascii() || self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Ignore non-ASCII characters or if scrolled
|
||||||
|
}
|
||||||
|
self.parser.process(&[s as u8]);
|
||||||
|
self.line.push(s);
|
||||||
|
CmdResult::Changed(self.state())
|
||||||
|
}
|
||||||
|
Cmd::Move(Direction::Down) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move down if not scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
self.history_next();
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Move(Direction::Left) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move up if not scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.line.left() {
|
||||||
|
self.parser.process(&[27, 91, 68]);
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Move(Direction::Right) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move up if not scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.line.right() {
|
||||||
|
self.parser.process(&[27, 91, 67]);
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Move(Direction::Up) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move up if not scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
self.history_prev();
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Cancel => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move to the beginning if scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.line.is_empty() {
|
||||||
|
self.line.backspace();
|
||||||
|
self.parser.process(&[8]); // Backspace character
|
||||||
|
// delete the last character from the line
|
||||||
|
// write one empty character to the terminal
|
||||||
|
self.parser.process(&[32]); // Space character
|
||||||
|
self.parser.process(&[8]); // Backspace character
|
||||||
|
}
|
||||||
|
CmdResult::Changed(self.state())
|
||||||
|
}
|
||||||
|
Cmd::Delete => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move to the beginning if scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.line.is_empty() {
|
||||||
|
self.line.delete();
|
||||||
|
self.parser.process(&[27, 91, 51, 126]); // Delete character
|
||||||
|
// write one empty character to the terminal
|
||||||
|
self.parser.process(&[32]); // Space character
|
||||||
|
self.parser.process(&[8]); // Backspace character
|
||||||
|
}
|
||||||
|
CmdResult::Changed(self.state())
|
||||||
|
}
|
||||||
|
Cmd::Scroll(Direction::Down) => {
|
||||||
|
self.scroll = self.scroll.saturating_sub(8);
|
||||||
|
self.parser.set_scrollback(self.scroll);
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Scroll(Direction::Up) => {
|
||||||
|
self.parser.set_scrollback(self.scroll.saturating_add(8));
|
||||||
|
let scrollback = self.parser.screen().scrollback();
|
||||||
|
self.scroll = scrollback;
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Toggle => {
|
||||||
|
// insert
|
||||||
|
self.parser.process(&[27, 91, 50, 126]); // Toggle insert mode
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::GoTo(Position::Begin) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move to the beginning if scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..self.line.begin() {
|
||||||
|
self.parser.process(&[27, 91, 68]); // Move cursor to the left
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::GoTo(Position::End) => {
|
||||||
|
if self.scroll > 0 {
|
||||||
|
return CmdResult::None; // Cannot move to the beginning if scrolled
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..self.line.end() {
|
||||||
|
self.parser.process(&[27, 91, 67]); // Move cursor to the right
|
||||||
|
}
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
Cmd::Submit => {
|
||||||
|
self.scroll = 0; // Reset scroll on submit
|
||||||
|
self.parser.set_scrollback(self.scroll);
|
||||||
|
|
||||||
|
if cfg!(target_family = "unix") {
|
||||||
|
self.parser.process(b"\n");
|
||||||
|
} else {
|
||||||
|
self.parser.process(b"\r\n\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = self.line.take();
|
||||||
|
if !line.is_empty() {
|
||||||
|
self.history.push(&line);
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdResult::Submit(State::One(StateValue::String(line)))
|
||||||
|
}
|
||||||
|
_ => CmdResult::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query(&self, attr: tuirealm::Attribute) -> Option<tuirealm::AttrValue> {
|
||||||
|
self.props.get(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::One(StateValue::String(self.line.content().to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&mut self, frame: &mut tuirealm::Frame, area: Rect) {
|
||||||
|
let width = area.width.saturating_sub(2);
|
||||||
|
let height = area.height.saturating_sub(2);
|
||||||
|
|
||||||
|
// update the terminal size if it has changed
|
||||||
|
if self.size != (width, height) {
|
||||||
|
self.size = (width, height);
|
||||||
|
self.parser.set_size(height, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = self
|
||||||
|
.query(Attribute::Title)
|
||||||
|
.map(|value| value.unwrap_string())
|
||||||
|
.unwrap_or_else(|| "Terminal".to_string());
|
||||||
|
|
||||||
|
let fg = self
|
||||||
|
.query(Attribute::Foreground)
|
||||||
|
.map(|value| value.unwrap_color())
|
||||||
|
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
||||||
|
|
||||||
|
let bg = self
|
||||||
|
.query(Attribute::Background)
|
||||||
|
.map(|value| value.unwrap_color())
|
||||||
|
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
||||||
|
|
||||||
|
let border_color = self
|
||||||
|
.query(Attribute::Borders)
|
||||||
|
.map(|value| value.unwrap_color())
|
||||||
|
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
||||||
|
|
||||||
|
let terminal = PseudoTerminal::new(self.parser.screen())
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(title)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(border_color))
|
||||||
|
.borders(BorderSides::ALL)
|
||||||
|
.style(Style::default().fg(fg).bg(bg)),
|
||||||
|
)
|
||||||
|
.style(Style::default().fg(fg).bg(bg));
|
||||||
|
|
||||||
|
frame.render_widget(terminal, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
/// Shell history management module.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct History {
|
||||||
|
/// Entries in the history.
|
||||||
|
entries: VecDeque<String>,
|
||||||
|
/// Maximum size of the history.
|
||||||
|
max_size: usize,
|
||||||
|
/// Current index in the history for navigation.
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl History {
|
||||||
|
/// Create a new [`History`] with a specified maximum size.
|
||||||
|
pub fn new(max_size: usize) -> Self {
|
||||||
|
History {
|
||||||
|
entries: VecDeque::with_capacity(max_size),
|
||||||
|
max_size,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a new command into the history.
|
||||||
|
pub fn push(&mut self, cmd: &str) {
|
||||||
|
if self.entries.len() == self.max_size {
|
||||||
|
self.entries.pop_front();
|
||||||
|
}
|
||||||
|
self.entries.push_back(cmd.to_string());
|
||||||
|
self.index = self.entries.len(); // Reset index to the end after adding a new command
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the previous command in the history.
|
||||||
|
///
|
||||||
|
/// Set also the index to the last command if it exists.
|
||||||
|
pub fn previous(&mut self) -> Option<String> {
|
||||||
|
if self.index > 0 {
|
||||||
|
self.index -= 1;
|
||||||
|
self.entries.get(self.index).cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the next command in the history.
|
||||||
|
///
|
||||||
|
/// Set also the index to the next command if it exists.
|
||||||
|
pub fn next(&mut self) -> Option<String> {
|
||||||
|
if self.index < self.entries.len() {
|
||||||
|
let cmd = self.entries.get(self.index).cloned();
|
||||||
|
self.index += 1;
|
||||||
|
cmd
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::History;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_history() {
|
||||||
|
let mut history = History::new(5);
|
||||||
|
history.push("first");
|
||||||
|
history.push("second");
|
||||||
|
history.push("third");
|
||||||
|
|
||||||
|
assert_eq!(history.previous(), Some("third".to_string()));
|
||||||
|
assert_eq!(history.previous(), Some("second".to_string()));
|
||||||
|
assert_eq!(history.previous(), Some("first".to_string()));
|
||||||
|
assert_eq!(history.previous(), None); // No more previous commands
|
||||||
|
assert_eq!(history.next(), Some("first".to_string()));
|
||||||
|
assert_eq!(history.next(), Some("second".to_string()));
|
||||||
|
assert_eq!(history.next(), Some("third".to_string()));
|
||||||
|
assert_eq!(history.next(), None); // No more next commands
|
||||||
|
history.push("fourth");
|
||||||
|
assert_eq!(history.previous(), Some("fourth".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
220
src/ui/activities/filetransfer/components/terminal/line.rs
Normal file
220
src/ui/activities/filetransfer/components/terminal/line.rs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/// A simple line for the shell, which keeps track of the current
|
||||||
|
/// content and the cursor position.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Line {
|
||||||
|
content: String,
|
||||||
|
cursor: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Line {
|
||||||
|
/// Set the content of the line and reset the cursor to the end.
|
||||||
|
pub fn set(&mut self, content: String) {
|
||||||
|
self.cursor = content.len();
|
||||||
|
self.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push a character to the line at the current cursor position.
|
||||||
|
pub fn push(&mut self, c: char) {
|
||||||
|
self.content.insert(self.cursor, c);
|
||||||
|
self.cursor += c.len_utf8();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take the current line content and reset the cursor.
|
||||||
|
pub fn take(&mut self) -> String {
|
||||||
|
self.cursor = 0;
|
||||||
|
std::mem::take(&mut self.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the current line content.
|
||||||
|
pub fn content(&self) -> &str {
|
||||||
|
&self.content
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the left, if possible.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the cursor was moved, `false` if it was already at the beginning.
|
||||||
|
pub fn left(&mut self) -> bool {
|
||||||
|
if self.cursor > 0 {
|
||||||
|
// get the previous character length
|
||||||
|
let prev_char_len = self
|
||||||
|
.content
|
||||||
|
.chars()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, c)| {
|
||||||
|
if i < self.cursor {
|
||||||
|
Some(c.len_utf8())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.last()
|
||||||
|
.unwrap();
|
||||||
|
self.cursor -= prev_char_len;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the right, if possible.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the cursor was moved, `false` if it was already at the end.
|
||||||
|
pub fn right(&mut self) -> bool {
|
||||||
|
if self.cursor < self.content.len() {
|
||||||
|
// get the next character length
|
||||||
|
let next_char_len = self.content[self.cursor..]
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.len_utf8();
|
||||||
|
self.cursor += next_char_len;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the beginning of the line.
|
||||||
|
///
|
||||||
|
/// Returns the previous cursor position.
|
||||||
|
pub fn begin(&mut self) -> usize {
|
||||||
|
std::mem::take(&mut self.cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the end of the line.
|
||||||
|
///
|
||||||
|
/// Returns the difference between the previous cursor position and the new position.
|
||||||
|
pub fn end(&mut self) -> usize {
|
||||||
|
let diff = self.content.len() - self.cursor;
|
||||||
|
self.cursor = self.content.len();
|
||||||
|
|
||||||
|
diff
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the previous character from the line at the current cursor position.
|
||||||
|
pub fn backspace(&mut self) {
|
||||||
|
if self.cursor > 0 {
|
||||||
|
let prev_char_len = self
|
||||||
|
.content
|
||||||
|
.chars()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, c)| {
|
||||||
|
if i < self.cursor {
|
||||||
|
Some(c.len_utf8())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.last()
|
||||||
|
.unwrap();
|
||||||
|
self.content.remove(self.cursor - prev_char_len);
|
||||||
|
self.cursor -= prev_char_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes the character at the current cursor position.
|
||||||
|
pub fn delete(&mut self) {
|
||||||
|
if self.cursor < self.content.len() {
|
||||||
|
self.content.remove(self.cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the line is empty.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.content.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_line() {
|
||||||
|
let mut line = Line::default();
|
||||||
|
assert!(line.is_empty());
|
||||||
|
|
||||||
|
line.push('H');
|
||||||
|
line.push('e');
|
||||||
|
line.push('l');
|
||||||
|
line.push('l');
|
||||||
|
line.push('o');
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
|
||||||
|
line.left();
|
||||||
|
line.left();
|
||||||
|
line.push(' ');
|
||||||
|
assert_eq!(line.content(), "Hel lo");
|
||||||
|
|
||||||
|
line.begin();
|
||||||
|
line.push('W');
|
||||||
|
assert_eq!(line.content(), "WHel lo");
|
||||||
|
|
||||||
|
line.end();
|
||||||
|
line.push('!');
|
||||||
|
assert_eq!(line.content(), "WHel lo!");
|
||||||
|
|
||||||
|
let taken = line.take();
|
||||||
|
assert_eq!(taken, "WHel lo!");
|
||||||
|
assert!(line.is_empty());
|
||||||
|
|
||||||
|
line.set("New Line".to_string());
|
||||||
|
assert_eq!(line.content(), "New Line");
|
||||||
|
|
||||||
|
line.backspace();
|
||||||
|
assert_eq!(line.content(), "New Lin");
|
||||||
|
line.left();
|
||||||
|
line.delete();
|
||||||
|
assert_eq!(line.content(), "New Li");
|
||||||
|
line.left();
|
||||||
|
line.left();
|
||||||
|
line.right();
|
||||||
|
assert_eq!(line.content(), "New Li");
|
||||||
|
line.end();
|
||||||
|
assert_eq!(line.content(), "New Li");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_return_whether_the_cursor_was_moved() {
|
||||||
|
let mut line = Line::default();
|
||||||
|
line.set("Hello".to_string());
|
||||||
|
|
||||||
|
assert!(line.left());
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
assert_eq!(line.cursor, 4);
|
||||||
|
|
||||||
|
assert!(line.left());
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
assert_eq!(line.cursor, 3);
|
||||||
|
|
||||||
|
assert!(line.right());
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
assert_eq!(line.cursor, 4);
|
||||||
|
assert!(line.right());
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
assert!(!line.right());
|
||||||
|
assert_eq!(line.cursor, 5);
|
||||||
|
assert!(!line.right());
|
||||||
|
|
||||||
|
line.end();
|
||||||
|
assert!(!line.right());
|
||||||
|
assert_eq!(line.content(), "Hello");
|
||||||
|
assert_eq!(line.cursor, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_allow_utf8_cursors() {
|
||||||
|
let mut line = Line::default();
|
||||||
|
line.set("Hello, 世界".to_string());
|
||||||
|
assert_eq!(line.content(), "Hello, 世界");
|
||||||
|
assert_eq!(line.cursor, 13); // "Hello, " is 7 bytes, "世界" is 6 bytes
|
||||||
|
|
||||||
|
assert!(line.left());
|
||||||
|
assert_eq!(line.content(), "Hello, 世界");
|
||||||
|
assert_eq!(line.cursor, 10); // Move left to '世'
|
||||||
|
assert!(line.left());
|
||||||
|
assert_eq!(line.content(), "Hello, 世界");
|
||||||
|
assert_eq!(line.cursor, 7); // Move left to ','
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -158,7 +158,7 @@ impl MockComponent for FileList {
|
|||||||
.props
|
.props
|
||||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
||||||
.unwrap_flag();
|
.unwrap_flag();
|
||||||
let div = tui_realm_stdlib::utils::get_block(borders, Some(title), focus, None);
|
let div = tui_realm_stdlib::utils::get_block(borders, Some(&title), focus, None);
|
||||||
// Make list entries
|
// Make list entries
|
||||||
let init_table_iter = if self.has_dot_dot() {
|
let init_table_iter = if self.has_dot_dot() {
|
||||||
vec![vec![TextSpan::from("..")]]
|
vec![vec![TextSpan::from("..")]]
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ impl FileListWithSearch {
|
|||||||
|
|
||||||
pub fn borders(mut self, b: Borders) -> Self {
|
pub fn borders(mut self, b: Borders) -> Self {
|
||||||
self.file_list
|
self.file_list
|
||||||
.attr(Attribute::Borders, AttrValue::Borders(b.clone()));
|
.attr(Attribute::Borders, AttrValue::Borders(b));
|
||||||
self.search.attr(Attribute::Borders, AttrValue::Borders(b));
|
self.search.attr(Attribute::Borders, AttrValue::Borders(b));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl ExplorerFuzzy {
|
|||||||
.foreground(fg)
|
.foreground(fg)
|
||||||
.highlighted_color(hg)
|
.highlighted_color(hg)
|
||||||
.title(title, Alignment::Left)
|
.title(title, Alignment::Left)
|
||||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
|
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +192,10 @@ impl ExplorerFuzzy {
|
|||||||
code: Key::Char('i'),
|
code: Key::Char('i'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
|
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('s'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}) => Some(Msg::Transfer(TransferMsg::GetFileSize)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('s') | Key::Function(2),
|
code: Key::Char('s') | Key::Function(2),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
@@ -236,7 +240,7 @@ impl ExplorerFind {
|
|||||||
.foreground(fg)
|
.foreground(fg)
|
||||||
.highlighted_color(hg)
|
.highlighted_color(hg)
|
||||||
.title(title, Alignment::Left)
|
.title(title, Alignment::Left)
|
||||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
|
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,6 +342,10 @@ impl Component<Msg, NoUserEvent> for ExplorerFind {
|
|||||||
code: Key::Char('i'),
|
code: Key::Char('i'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
|
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('s'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}) => Some(Msg::Transfer(TransferMsg::GetFileSize)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('s') | Key::Function(2),
|
code: Key::Char('s') | Key::Function(2),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
@@ -373,7 +381,7 @@ impl ExplorerLocal {
|
|||||||
.foreground(fg)
|
.foreground(fg)
|
||||||
.highlighted_color(hg)
|
.highlighted_color(hg)
|
||||||
.title(title, Alignment::Left)
|
.title(title, Alignment::Left)
|
||||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect())
|
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect())
|
||||||
.dot_dot(true),
|
.dot_dot(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,6 +536,10 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
|
|||||||
code: Key::Char('r') | Key::Function(6),
|
code: Key::Char('r') | Key::Function(6),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::Ui(UiMsg::ShowRenamePopup)),
|
}) => Some(Msg::Ui(UiMsg::ShowRenamePopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('s'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}) => Some(Msg::Transfer(TransferMsg::GetFileSize)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('s') | Key::Function(2),
|
code: Key::Char('s') | Key::Function(2),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
@@ -547,7 +559,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
|
|||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('x'),
|
code: Key::Char('x'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::Ui(UiMsg::ShowExecPopup)),
|
}) => Some(Msg::Ui(UiMsg::ShowTerminal)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('y'),
|
code: Key::Char('y'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
@@ -587,7 +599,7 @@ impl ExplorerRemote {
|
|||||||
.foreground(fg)
|
.foreground(fg)
|
||||||
.highlighted_color(hg)
|
.highlighted_color(hg)
|
||||||
.title(title, Alignment::Left)
|
.title(title, Alignment::Left)
|
||||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect())
|
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect())
|
||||||
.dot_dot(true),
|
.dot_dot(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -742,6 +754,10 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
|
|||||||
code: Key::Char('r') | Key::Function(6),
|
code: Key::Char('r') | Key::Function(6),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::Ui(UiMsg::ShowRenamePopup)),
|
}) => Some(Msg::Ui(UiMsg::ShowRenamePopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('s'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}) => Some(Msg::Transfer(TransferMsg::GetFileSize)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('s') | Key::Function(2),
|
code: Key::Char('s') | Key::Function(2),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
@@ -761,7 +777,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
|
|||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('x'),
|
code: Key::Char('x'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::Ui(UiMsg::ShowExecPopup)),
|
}) => Some(Msg::Ui(UiMsg::ShowTerminal)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('y'),
|
code: Key::Char('y'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
|
|||||||
@@ -148,6 +148,25 @@ impl Browser {
|
|||||||
self.sync_browsing = !self.sync_browsing;
|
self.sync_browsing = !self.sync_browsing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle terminal for the current tab
|
||||||
|
pub fn toggle_terminal(&mut self, terminal: bool) {
|
||||||
|
if self.tab == FileExplorerTab::HostBridge {
|
||||||
|
self.host_bridge.toggle_terminal(terminal);
|
||||||
|
} else if self.tab == FileExplorerTab::Remote {
|
||||||
|
self.remote.toggle_terminal(terminal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if terminal is open for the host bridge tab
|
||||||
|
pub fn is_terminal_open_host_bridge(&self) -> bool {
|
||||||
|
self.tab == FileExplorerTab::HostBridge && self.host_bridge.terminal_open()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if terminal is open for the remote tab
|
||||||
|
pub fn is_terminal_open_remote(&self) -> bool {
|
||||||
|
self.tab == FileExplorerTab::Remote && self.remote.terminal_open()
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a file explorer with local host setup
|
/// Build a file explorer with local host setup
|
||||||
pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer {
|
pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer {
|
||||||
let mut builder = Self::build_explorer(cli);
|
let mut builder = Self::build_explorer(cli);
|
||||||
|
|||||||
@@ -304,6 +304,70 @@ impl FileTransferActivity {
|
|||||||
self.reload_remote_filelist();
|
self.reload_remote_filelist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_tab_hostname(&self) -> String {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||||
|
self.get_hostbridge_hostname()
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.get_remote_hostname(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn terminal_prompt(&self) -> String {
|
||||||
|
const TERM_CYAN: &str = "\x1b[36m";
|
||||||
|
const TERM_GREEN: &str = "\x1b[32m";
|
||||||
|
const TERM_YELLOW: &str = "\x1b[33m";
|
||||||
|
const TERM_RESET: &str = "\x1b[0m";
|
||||||
|
|
||||||
|
let panel = self.browser.tab();
|
||||||
|
match panel {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||||
|
let username = self
|
||||||
|
.context()
|
||||||
|
.host_bridge_params()
|
||||||
|
.and_then(|params| {
|
||||||
|
params
|
||||||
|
.username()
|
||||||
|
.map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@"))
|
||||||
|
})
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let hostname = self.get_hostbridge_hostname();
|
||||||
|
format!(
|
||||||
|
"{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{}{TERM_RESET}$ ",
|
||||||
|
fmt_path_elide_ex(
|
||||||
|
self.host_bridge().wrkdir.as_path(),
|
||||||
|
0,
|
||||||
|
hostname.len() + 3 // 3 because of '/…/'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
|
||||||
|
let username = self
|
||||||
|
.context()
|
||||||
|
.remote_params()
|
||||||
|
.and_then(|params| {
|
||||||
|
params
|
||||||
|
.username()
|
||||||
|
.map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@"))
|
||||||
|
})
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let hostname = self.get_remote_hostname();
|
||||||
|
let fmt_path = fmt_path_elide_ex(
|
||||||
|
self.remote().wrkdir.as_path(),
|
||||||
|
0,
|
||||||
|
hostname.len() + 3, // 3 because of '/…/'
|
||||||
|
);
|
||||||
|
let fmt_path = if fmt_path.starts_with('/') {
|
||||||
|
fmt_path
|
||||||
|
} else {
|
||||||
|
format!("/{}", fmt_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{fmt_path}{TERM_RESET}$ ",)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn reload_remote_filelist(&mut self) {
|
pub(super) fn reload_remote_filelist(&mut self) {
|
||||||
let width = self
|
let width = self
|
||||||
.context_mut()
|
.context_mut()
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ enum Id {
|
|||||||
DeletePopup,
|
DeletePopup,
|
||||||
DisconnectPopup,
|
DisconnectPopup,
|
||||||
ErrorPopup,
|
ErrorPopup,
|
||||||
ExecPopup,
|
|
||||||
ExplorerFind,
|
ExplorerFind,
|
||||||
ExplorerHostBridge,
|
ExplorerHostBridge,
|
||||||
ExplorerRemote,
|
ExplorerRemote,
|
||||||
@@ -73,13 +72,14 @@ enum Id {
|
|||||||
QuitPopup,
|
QuitPopup,
|
||||||
RenamePopup,
|
RenamePopup,
|
||||||
ReplacePopup,
|
ReplacePopup,
|
||||||
ReplacingFilesListPopup,
|
|
||||||
SaveAsPopup,
|
SaveAsPopup,
|
||||||
SortingPopup,
|
SortingPopup,
|
||||||
StatusBarHostBridge,
|
StatusBarHostBridge,
|
||||||
StatusBarRemote,
|
StatusBarRemote,
|
||||||
SymlinkPopup,
|
SymlinkPopup,
|
||||||
SyncBrowsingMkdirPopup,
|
SyncBrowsingMkdirPopup,
|
||||||
|
TerminalHostBridge,
|
||||||
|
TerminalRemote,
|
||||||
TransferQueueHostBridge,
|
TransferQueueHostBridge,
|
||||||
TransferQueueRemote,
|
TransferQueueRemote,
|
||||||
WaitPopup,
|
WaitPopup,
|
||||||
@@ -97,10 +97,14 @@ enum Msg {
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum PendingActionMsg {
|
enum PendingActionMsg {
|
||||||
CloseReplacePopups,
|
|
||||||
CloseSyncBrowsingMkdirPopup,
|
CloseSyncBrowsingMkdirPopup,
|
||||||
MakePendingDirectory,
|
MakePendingDirectory,
|
||||||
TransferPendingFile,
|
/// Replace file popup
|
||||||
|
ReplaceCancel,
|
||||||
|
ReplaceOverwrite,
|
||||||
|
ReplaceOverwriteAll,
|
||||||
|
ReplaceSkip,
|
||||||
|
ReplaceSkipAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
@@ -113,6 +117,7 @@ enum TransferMsg {
|
|||||||
DeleteFile,
|
DeleteFile,
|
||||||
EnterDirectory,
|
EnterDirectory,
|
||||||
ExecuteCmd(String),
|
ExecuteCmd(String),
|
||||||
|
GetFileSize,
|
||||||
GoTo(String),
|
GoTo(String),
|
||||||
GoToParentDirectory,
|
GoToParentDirectory,
|
||||||
GoToPreviousDirectory,
|
GoToPreviousDirectory,
|
||||||
@@ -170,13 +175,13 @@ enum UiMsg {
|
|||||||
MarkAll,
|
MarkAll,
|
||||||
/// Clear all marks
|
/// Clear all marks
|
||||||
MarkClear,
|
MarkClear,
|
||||||
|
|
||||||
Quit,
|
Quit,
|
||||||
ReplacePopupTabbed,
|
|
||||||
ShowChmodPopup,
|
ShowChmodPopup,
|
||||||
ShowCopyPopup,
|
ShowCopyPopup,
|
||||||
ShowDeletePopup,
|
ShowDeletePopup,
|
||||||
ShowDisconnectPopup,
|
ShowDisconnectPopup,
|
||||||
ShowExecPopup,
|
ShowTerminal,
|
||||||
ShowFileInfoPopup,
|
ShowFileInfoPopup,
|
||||||
ShowFileSortingPopup,
|
ShowFileSortingPopup,
|
||||||
ShowFilterPopup,
|
ShowFilterPopup,
|
||||||
@@ -286,10 +291,7 @@ impl FileTransferActivity {
|
|||||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||||
walkdir: WalkdirStates::default(),
|
walkdir: WalkdirStates::default(),
|
||||||
transfer: TransferStates::default(),
|
transfer: TransferStates::default(),
|
||||||
cache: match TempDir::new() {
|
cache: TempDir::new().ok(),
|
||||||
Ok(d) => Some(d),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
fswatcher: if enable_fs_watcher {
|
fswatcher: if enable_fs_watcher {
|
||||||
FsWatcher::init(Duration::from_secs(5)).ok()
|
FsWatcher::init(Duration::from_secs(5)).ok()
|
||||||
} else {
|
} else {
|
||||||
@@ -508,10 +510,10 @@ impl Activity for FileTransferActivity {
|
|||||||
/// This function must be called once before terminating the activity.
|
/// This function must be called once before terminating the activity.
|
||||||
fn on_destroy(&mut self) -> Option<Context> {
|
fn on_destroy(&mut self) -> Option<Context> {
|
||||||
// Destroy cache
|
// Destroy cache
|
||||||
if let Some(cache) = self.cache.take() {
|
if let Some(cache) = self.cache.take()
|
||||||
if let Err(err) = cache.close() {
|
&& let Err(err) = cache.close()
|
||||||
error!("Failed to delete cache: {}", err);
|
{
|
||||||
}
|
error!("Failed to delete cache: {}", err);
|
||||||
}
|
}
|
||||||
// Disable raw mode
|
// Disable raw mode
|
||||||
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
|
if let Err(err) = self.context_mut().terminal().disable_raw_mode() {
|
||||||
|
|||||||
@@ -1260,7 +1260,10 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
/// Get total size of transfer for host_bridgehost
|
/// Get total size of transfer for host_bridgehost
|
||||||
fn get_total_transfer_size_host(&mut self, entry: &File) -> usize {
|
fn get_total_transfer_size_host(&mut self, entry: &File) -> usize {
|
||||||
if entry.is_dir() {
|
// mount message to tell we are calculating size
|
||||||
|
self.mount_blocking_wait("Calculating transfer size…");
|
||||||
|
|
||||||
|
let sz = if entry.is_dir() {
|
||||||
// List dir
|
// List dir
|
||||||
match self.host_bridge.list_dir(entry.path()) {
|
match self.host_bridge.list_dir(entry.path()) {
|
||||||
Ok(files) => files
|
Ok(files) => files
|
||||||
@@ -1281,12 +1284,18 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
entry.metadata.size as usize
|
entry.metadata.size as usize
|
||||||
}
|
};
|
||||||
|
self.umount_wait();
|
||||||
|
|
||||||
|
sz
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get total size of transfer for remote host
|
/// Get total size of transfer for remote host
|
||||||
fn get_total_transfer_size_remote(&mut self, entry: &File) -> usize {
|
fn get_total_transfer_size_remote(&mut self, entry: &File) -> usize {
|
||||||
if entry.is_dir() {
|
// mount message to tell we are calculating size
|
||||||
|
self.mount_blocking_wait("Calculating transfer size…");
|
||||||
|
|
||||||
|
let sz = if entry.is_dir() {
|
||||||
// List directory
|
// List directory
|
||||||
match self.client.list_dir(entry.path()) {
|
match self.client.list_dir(entry.path()) {
|
||||||
Ok(files) => files
|
Ok(files) => files
|
||||||
@@ -1307,7 +1316,11 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
entry.metadata.size as usize
|
entry.metadata.size as usize
|
||||||
}
|
};
|
||||||
|
|
||||||
|
self.umount_wait();
|
||||||
|
|
||||||
|
sz
|
||||||
}
|
}
|
||||||
|
|
||||||
// file changed
|
// file changed
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
// locals
|
// locals
|
||||||
// externals
|
// externals
|
||||||
use remotefs::fs::File;
|
use remotefs::fs::File;
|
||||||
use tuirealm::props::{AttrValue, Attribute};
|
|
||||||
use tuirealm::{State, StateValue, Update};
|
use tuirealm::{State, StateValue, Update};
|
||||||
|
|
||||||
use super::actions::SelectedFile;
|
use super::actions::SelectedFile;
|
||||||
@@ -146,17 +145,15 @@ impl FileTransferActivity {
|
|||||||
self.update_browser_file_list()
|
self.update_browser_file_list()
|
||||||
}
|
}
|
||||||
TransferMsg::ExecuteCmd(cmd) => {
|
TransferMsg::ExecuteCmd(cmd) => {
|
||||||
// Exex command
|
// Exec command
|
||||||
self.umount_exec();
|
|
||||||
self.mount_blocking_wait(format!("Executing '{cmd}'…").as_str());
|
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::HostBridge => self.action_local_exec(cmd),
|
FileExplorerTab::HostBridge => self.action_local_exec(cmd),
|
||||||
FileExplorerTab::Remote => self.action_remote_exec(cmd),
|
FileExplorerTab::Remote => self.action_remote_exec(cmd),
|
||||||
_ => panic!("Found tab doesn't support EXEC"),
|
_ => panic!("Found tab doesn't support EXEC"),
|
||||||
}
|
};
|
||||||
self.umount_wait();
|
}
|
||||||
// Reload files
|
TransferMsg::GetFileSize => {
|
||||||
self.update_browser_file_list()
|
self.action_get_file_size();
|
||||||
}
|
}
|
||||||
TransferMsg::GoTo(dir) => {
|
TransferMsg::GoTo(dir) => {
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
@@ -417,7 +414,10 @@ impl FileTransferActivity {
|
|||||||
UiMsg::CloseDeletePopup => self.umount_radio_delete(),
|
UiMsg::CloseDeletePopup => self.umount_radio_delete(),
|
||||||
UiMsg::CloseDisconnectPopup => self.umount_disconnect(),
|
UiMsg::CloseDisconnectPopup => self.umount_disconnect(),
|
||||||
UiMsg::CloseErrorPopup => self.umount_error(),
|
UiMsg::CloseErrorPopup => self.umount_error(),
|
||||||
UiMsg::CloseExecPopup => self.umount_exec(),
|
UiMsg::CloseExecPopup => {
|
||||||
|
self.browser.toggle_terminal(false);
|
||||||
|
self.umount_exec();
|
||||||
|
}
|
||||||
UiMsg::CloseFatalPopup => {
|
UiMsg::CloseFatalPopup => {
|
||||||
self.umount_fatal();
|
self.umount_fatal();
|
||||||
self.exit_reason = Some(ExitReason::Disconnect);
|
self.exit_reason = Some(ExitReason::Disconnect);
|
||||||
@@ -506,15 +506,6 @@ impl FileTransferActivity {
|
|||||||
self.disconnect_and_quit();
|
self.disconnect_and_quit();
|
||||||
self.umount_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 => {
|
UiMsg::ShowChmodPopup => {
|
||||||
let selected_file = match self.browser.tab() {
|
let selected_file = match self.browser.tab() {
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
@@ -546,7 +537,10 @@ impl FileTransferActivity {
|
|||||||
UiMsg::ShowCopyPopup => self.mount_copy(),
|
UiMsg::ShowCopyPopup => self.mount_copy(),
|
||||||
UiMsg::ShowDeletePopup => self.mount_radio_delete(),
|
UiMsg::ShowDeletePopup => self.mount_radio_delete(),
|
||||||
UiMsg::ShowDisconnectPopup => self.mount_disconnect(),
|
UiMsg::ShowDisconnectPopup => self.mount_disconnect(),
|
||||||
UiMsg::ShowExecPopup => self.mount_exec(),
|
UiMsg::ShowTerminal => {
|
||||||
|
self.browser.toggle_terminal(true);
|
||||||
|
self.mount_exec()
|
||||||
|
}
|
||||||
UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::HostBridge => {
|
UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::HostBridge => {
|
||||||
if let SelectedFile::One(file) = self.get_local_selected_entries() {
|
if let SelectedFile::One(file) = self.get_local_selected_entries() {
|
||||||
self.mount_file_info(&file);
|
self.mount_file_info(&file);
|
||||||
|
|||||||
@@ -158,12 +158,16 @@ impl FileTransferActivity {
|
|||||||
// @! Local explorer (Find or default)
|
// @! Local explorer (Find or default)
|
||||||
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) {
|
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) {
|
||||||
self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]);
|
self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]);
|
||||||
|
} else if self.browser.is_terminal_open_host_bridge() {
|
||||||
|
self.app.view(&Id::TerminalHostBridge, f, tabs_chunks[0]);
|
||||||
} else {
|
} else {
|
||||||
self.app.view(&Id::ExplorerHostBridge, f, tabs_chunks[0]);
|
self.app.view(&Id::ExplorerHostBridge, f, tabs_chunks[0]);
|
||||||
}
|
}
|
||||||
// @! Remote explorer (Find or default)
|
// @! Remote explorer (Find or default)
|
||||||
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) {
|
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) {
|
||||||
self.app.view(&Id::ExplorerFind, f, tabs_chunks[1]);
|
self.app.view(&Id::ExplorerFind, f, tabs_chunks[1]);
|
||||||
|
} else if self.browser.is_terminal_open_remote() {
|
||||||
|
self.app.view(&Id::TerminalRemote, f, tabs_chunks[1]);
|
||||||
} else {
|
} else {
|
||||||
self.app.view(&Id::ExplorerRemote, f, tabs_chunks[1]);
|
self.app.view(&Id::ExplorerRemote, f, tabs_chunks[1]);
|
||||||
}
|
}
|
||||||
@@ -238,13 +242,8 @@ impl FileTransferActivity {
|
|||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.app.view(&Id::SymlinkPopup, f, popup);
|
self.app.view(&Id::SymlinkPopup, f, popup);
|
||||||
} else if self.app.mounted(&Id::ExecPopup) {
|
|
||||||
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area());
|
|
||||||
f.render_widget(Clear, popup);
|
|
||||||
// make popup
|
|
||||||
self.app.view(&Id::ExecPopup, f, popup);
|
|
||||||
} else if self.app.mounted(&Id::FileInfoPopup) {
|
} else if self.app.mounted(&Id::FileInfoPopup) {
|
||||||
let popup = Popup(Size::Percentage(50), Size::Percentage(50)).draw_in(f.area());
|
let popup = Popup(Size::Percentage(80), Size::Percentage(50)).draw_in(f.area());
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.app.view(&Id::FileInfoPopup, f, popup);
|
self.app.view(&Id::FileInfoPopup, f, popup);
|
||||||
@@ -270,29 +269,10 @@ impl FileTransferActivity {
|
|||||||
// make popup
|
// make popup
|
||||||
self.app.view(&Id::DeletePopup, f, popup);
|
self.app.view(&Id::DeletePopup, f, popup);
|
||||||
} else if self.app.mounted(&Id::ReplacePopup) {
|
} else if self.app.mounted(&Id::ReplacePopup) {
|
||||||
// NOTE: handle extended / normal modes
|
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area());
|
||||||
if self.is_radio_replace_extended() {
|
f.render_widget(Clear, popup);
|
||||||
let popup = Popup(Size::Percentage(50), Size::Percentage(50)).draw_in(f.area());
|
// make popup
|
||||||
f.render_widget(Clear, popup);
|
self.app.view(&Id::ReplacePopup, f, popup);
|
||||||
let popup_chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Percentage(85), // List
|
|
||||||
Constraint::Percentage(15), // Radio
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(popup);
|
|
||||||
self.app
|
|
||||||
.view(&Id::ReplacingFilesListPopup, f, popup_chunks[0]);
|
|
||||||
self.app.view(&Id::ReplacePopup, f, popup_chunks[1]);
|
|
||||||
} else {
|
|
||||||
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area());
|
|
||||||
f.render_widget(Clear, popup);
|
|
||||||
// make popup
|
|
||||||
self.app.view(&Id::ReplacePopup, f, popup);
|
|
||||||
}
|
|
||||||
} else if self.app.mounted(&Id::DisconnectPopup) {
|
} else if self.app.mounted(&Id::DisconnectPopup) {
|
||||||
let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area());
|
let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area());
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
@@ -570,21 +550,69 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_exec(&mut self) {
|
pub(super) fn mount_exec(&mut self) {
|
||||||
|
let tab = self.browser.tab();
|
||||||
|
let id = match tab {
|
||||||
|
FileExplorerTab::HostBridge => Id::TerminalHostBridge,
|
||||||
|
FileExplorerTab::Remote => Id::TerminalRemote,
|
||||||
|
_ => panic!("Cannot mount terminal on this tab"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let border = match tab {
|
||||||
|
FileExplorerTab::HostBridge => self.theme().transfer_local_explorer_highlighted,
|
||||||
|
FileExplorerTab::Remote => self.theme().transfer_remote_explorer_highlighted,
|
||||||
|
_ => panic!("Cannot mount terminal on this tab"),
|
||||||
|
};
|
||||||
|
|
||||||
let input_color = self.theme().misc_input_dialog;
|
let input_color = self.theme().misc_input_dialog;
|
||||||
assert!(
|
assert!(
|
||||||
self.app
|
self.app
|
||||||
.remount(
|
.remount(
|
||||||
Id::ExecPopup,
|
id.clone(),
|
||||||
Box::new(components::ExecPopup::new(input_color)),
|
Box::new(
|
||||||
|
components::Terminal::default()
|
||||||
|
.foreground(input_color)
|
||||||
|
.prompt(self.terminal_prompt())
|
||||||
|
.title(format!("Terminal - {}", self.get_tab_hostname()))
|
||||||
|
.border_color(border)
|
||||||
|
),
|
||||||
vec![],
|
vec![],
|
||||||
)
|
)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
);
|
);
|
||||||
assert!(self.app.active(&Id::ExecPopup).is_ok());
|
assert!(self.app.active(&id).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the terminal prompt based on the current directory
|
||||||
|
pub(super) fn update_terminal_prompt(&mut self) {
|
||||||
|
let prompt = self.terminal_prompt();
|
||||||
|
let id = match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge => Id::TerminalHostBridge,
|
||||||
|
FileExplorerTab::Remote => Id::TerminalRemote,
|
||||||
|
_ => panic!("Cannot update terminal prompt on this tab"),
|
||||||
|
};
|
||||||
|
let _ = self
|
||||||
|
.app
|
||||||
|
.attr(&id, Attribute::Content, AttrValue::String(prompt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print output to terminal
|
||||||
|
pub(super) fn print_terminal(&mut self, text: String) {
|
||||||
|
// get id
|
||||||
|
let focus = self.app.focus().unwrap().clone();
|
||||||
|
|
||||||
|
// replace all \n with \r\n
|
||||||
|
let mut text = text.replace('\n', "\r\n");
|
||||||
|
if !text.ends_with("\r\n") && !text.is_empty() {
|
||||||
|
text.push_str("\r\n");
|
||||||
|
}
|
||||||
|
let _ = self
|
||||||
|
.app
|
||||||
|
.attr(&focus, Attribute::Text, AttrValue::String(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn umount_exec(&mut self) {
|
pub(super) fn umount_exec(&mut self) {
|
||||||
let _ = self.app.umount(&Id::ExecPopup);
|
let focus = self.app.focus().unwrap().clone();
|
||||||
|
let _ = self.app.umount(&focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) {
|
pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) {
|
||||||
@@ -897,37 +925,8 @@ impl FileTransferActivity {
|
|||||||
assert!(self.app.active(&Id::ReplacePopup).is_ok());
|
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) {
|
pub(super) fn umount_radio_replace(&mut self) {
|
||||||
let _ = self.app.umount(&Id::ReplacePopup);
|
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) {
|
pub(super) fn mount_file_info(&mut self, file: &File) {
|
||||||
@@ -1102,6 +1101,10 @@ impl FileTransferActivity {
|
|||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
|
if x.as_payload().is_none() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
x.unwrap_payload()
|
x.unwrap_payload()
|
||||||
.unwrap_vec()
|
.unwrap_vec()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -1179,7 +1182,8 @@ impl FileTransferActivity {
|
|||||||
Id::DeletePopup,
|
Id::DeletePopup,
|
||||||
Id::DisconnectPopup,
|
Id::DisconnectPopup,
|
||||||
Id::ErrorPopup,
|
Id::ErrorPopup,
|
||||||
Id::ExecPopup,
|
Id::TerminalHostBridge,
|
||||||
|
Id::TerminalRemote,
|
||||||
Id::FatalPopup,
|
Id::FatalPopup,
|
||||||
Id::FileInfoPopup,
|
Id::FileInfoPopup,
|
||||||
Id::GotoPopup,
|
Id::GotoPopup,
|
||||||
|
|||||||
@@ -99,27 +99,14 @@ impl SetupActivity {
|
|||||||
Ok(State::One(StateValue::Usize(idx))) => Some(idx),
|
Ok(State::One(StateValue::Usize(idx))) => Some(idx),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(idx) = idx {
|
// get ssh key and delete it
|
||||||
let key: Option<String> = self.config().iter_ssh_keys().nth(idx).cloned();
|
if let Some(Err(err)) = idx
|
||||||
if let Some(key) = key {
|
.and_then(|i| self.config().iter_ssh_keys().nth(i).cloned())
|
||||||
match self.config().get_ssh_key(&key) {
|
.and_then(|key| self.config().get_ssh_key(&key))
|
||||||
Ok(opt) => {
|
.map(|(host, username, _)| self.delete_ssh_key(host.as_str(), username.as_str()))
|
||||||
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());
|
||||||
// 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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ impl ErrorPopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(Color::Red)
|
.foreground(Color::Red)
|
||||||
.text(&[TextSpan::from(text.as_ref())])
|
.text([TextSpan::from(text.as_ref())])
|
||||||
.wrap(true),
|
.wrap(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ pub struct Footer {
|
|||||||
impl Default for Footer {
|
impl Default for Footer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Span::default().spans(&[
|
component: Span::default().spans([
|
||||||
TextSpan::new("<F1|CTRL+H>").bold().fg(Color::Cyan),
|
TextSpan::new("<F1|CTRL+H>").bold().fg(Color::Cyan),
|
||||||
TextSpan::new(" Help "),
|
TextSpan::new(" Help "),
|
||||||
TextSpan::new("<F4|CTRL+S>").bold().fg(Color::Cyan),
|
TextSpan::new("<F4|CTRL+S>").bold().fg(Color::Cyan),
|
||||||
@@ -88,7 +88,7 @@ impl Header {
|
|||||||
.color(Color::Yellow)
|
.color(Color::Yellow)
|
||||||
.sides(BorderSides::BOTTOM),
|
.sides(BorderSides::BOTTOM),
|
||||||
)
|
)
|
||||||
.choices(&["Configuration parameters", "SSH Keys", "Theme"])
|
.choices(["Configuration parameters", "SSH Keys", "Theme"])
|
||||||
.foreground(Color::Yellow)
|
.foreground(Color::Yellow)
|
||||||
.value(match layout {
|
.value(match layout {
|
||||||
ViewLayout::SetupForm => 0,
|
ViewLayout::SetupForm => 0,
|
||||||
@@ -217,7 +217,7 @@ impl Default for QuitPopup {
|
|||||||
Alignment::Center,
|
Alignment::Center,
|
||||||
)
|
)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.choices(&["Save", "Don't save", "Cancel"]),
|
.choices(["Save", "Don't save", "Cancel"]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,7 +273,7 @@ impl Default for SavePopup {
|
|||||||
.foreground(Color::Yellow)
|
.foreground(Color::Yellow)
|
||||||
.title("Save changes?", Alignment::Center)
|
.title("Save changes?", Alignment::Center)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.choices(&["Yes", "No"]),
|
.choices(["Yes", "No"]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ impl CheckUpdates {
|
|||||||
.color(Color::LightYellow)
|
.color(Color::LightYellow)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.foreground(Color::LightYellow)
|
.foreground(Color::LightYellow)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.title("Check for updates?", Alignment::Left)
|
.title("Check for updates?", Alignment::Left)
|
||||||
@@ -67,7 +67,7 @@ impl DefaultProtocol {
|
|||||||
.color(Color::Cyan)
|
.color(Color::Cyan)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["SFTP", "SCP", "FTP", "FTPS", "Kube", "S3", "SMB", "WebDAV"])
|
.choices(["SFTP", "SCP", "FTP", "FTPS", "Kube", "S3", "SMB", "WebDAV"])
|
||||||
.foreground(Color::Cyan)
|
.foreground(Color::Cyan)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.title("Default protocol", Alignment::Left)
|
.title("Default protocol", Alignment::Left)
|
||||||
@@ -110,7 +110,7 @@ impl GroupDirs {
|
|||||||
.color(Color::LightMagenta)
|
.color(Color::LightMagenta)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["Display first", "Display last", "No"])
|
.choices(["Display first", "Display last", "No"])
|
||||||
.foreground(Color::LightMagenta)
|
.foreground(Color::LightMagenta)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.title("Group directories", Alignment::Left)
|
.title("Group directories", Alignment::Left)
|
||||||
@@ -148,7 +148,7 @@ impl HiddenFiles {
|
|||||||
.color(Color::LightRed)
|
.color(Color::LightRed)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.foreground(Color::LightRed)
|
.foreground(Color::LightRed)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.title("Show hidden files? (by default)", Alignment::Left)
|
.title("Show hidden files? (by default)", Alignment::Left)
|
||||||
@@ -182,7 +182,7 @@ impl NotificationsEnabled {
|
|||||||
.color(Color::LightRed)
|
.color(Color::LightRed)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.foreground(Color::LightRed)
|
.foreground(Color::LightRed)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.title("Enable notifications?", Alignment::Left)
|
.title("Enable notifications?", Alignment::Left)
|
||||||
@@ -216,7 +216,7 @@ impl PromptOnFileReplace {
|
|||||||
.color(Color::LightBlue)
|
.color(Color::LightBlue)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.foreground(Color::LightBlue)
|
.foreground(Color::LightBlue)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.title("Prompt when replacing existing files?", Alignment::Left)
|
.title("Prompt when replacing existing files?", Alignment::Left)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ impl Default for DelSshKeyPopup {
|
|||||||
.color(Color::Red)
|
.color(Color::Red)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.choices(&["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.foreground(Color::Red)
|
.foreground(Color::Red)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.title("Delete key?", Alignment::Center)
|
.title("Delete key?", Alignment::Center)
|
||||||
|
|||||||
@@ -77,16 +77,11 @@ impl SetupActivity {
|
|||||||
Some(key) => {
|
Some(key) => {
|
||||||
// Get key path
|
// Get key path
|
||||||
match ctx.config().get_ssh_key(key) {
|
match ctx.config().get_ssh_key(key) {
|
||||||
Ok(ssh_key) => match ssh_key {
|
None => Ok(()),
|
||||||
None => Ok(()),
|
Some((_, _, key_path)) => match edit::edit_file(key_path.as_path()) {
|
||||||
Some((_, _, key_path)) => {
|
Ok(_) => Ok(()),
|
||||||
match edit::edit_file(key_path.as_path()) {
|
Err(err) => Err(format!("Could not edit ssh key: {err}")),
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(err) => Err(format!("Could not edit ssh key: {err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Err(err) => Err(format!("Could not read ssh key: {err}")),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Ok(()),
|
None => Ok(()),
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ impl SetupActivity {
|
|||||||
.config()
|
.config()
|
||||||
.iter_ssh_keys()
|
.iter_ssh_keys()
|
||||||
.map(|x| {
|
.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}")
|
format!("{username} at {addr}")
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ static REMOTE_SMB_OPT_REGEX: Lazy<Regex> =
|
|||||||
/**
|
/**
|
||||||
* Regex matches:
|
* Regex matches:
|
||||||
* - group 1: Version
|
* - group 1: Version
|
||||||
* E.g. termscp-0.3.2 => 0.3.2; v0.4.0 => 0.4.0
|
* E.g. termscp-0.3.2 => 0.3.2; v0.4.0 => 0.4.0
|
||||||
*/
|
*/
|
||||||
static SEMVER_REGEX: Lazy<Regex> = lazy_regex!(r"v?((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*))");
|
static SEMVER_REGEX: Lazy<Regex> = lazy_regex!(r"v?((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*))");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user