mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Compare commits
34 Commits
v0.16.1
...
67a14c2725
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67a14c2725 | ||
|
|
df03c5c1bf | ||
|
|
3ce3ffee3d | ||
|
|
c0b32a1847 | ||
|
|
81ae0035c3 | ||
|
|
783da22ca2 | ||
|
|
8715c2b6f9 | ||
|
|
98a748dccc | ||
|
|
bef031a414 | ||
|
|
ce0e953182 | ||
|
|
9a5caf75c3 | ||
|
|
446f4a3a32 | ||
|
|
da75912d26 | ||
|
|
4ed23c2a18 | ||
|
|
23ae334bef | ||
|
|
ec75ae1486 | ||
|
|
368570592f | ||
|
|
806793421e | ||
|
|
1f377b242d | ||
|
|
a4906b129a | ||
|
|
5c6e8925ad | ||
|
|
a18eff689d | ||
|
|
274742d6d9 | ||
|
|
cdebcbd4dc | ||
|
|
cdf303a847 | ||
|
|
dd35fe825c | ||
|
|
5c4a971aca | ||
|
|
7522b4d0ff | ||
|
|
b0f314837e | ||
|
|
8a9ba7745a | ||
|
|
b7d75a2749 | ||
|
|
fe0d9b0aa6 | ||
|
|
7dba691ccc | ||
|
|
099e2154ba |
8
.github/workflows/build-artifacts.yml
vendored
8
.github/workflows/build-artifacts.yml
vendored
@@ -3,6 +3,9 @@ name: "Build artifacts"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TERMSCP_VERSION: "0.18.0"
|
||||
|
||||
jobs:
|
||||
build-binaries:
|
||||
name: Build - ${{ matrix.platform.release_for }}
|
||||
@@ -32,11 +35,12 @@ jobs:
|
||||
run: |
|
||||
mkdir -p .artifact
|
||||
mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp
|
||||
tar -czf .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz -C .artifact termscp
|
||||
ls -l .artifact/
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
name: ${{ matrix.platform.release_for }}
|
||||
path: .artifact/termscp
|
||||
name: termscp-${{ matrix.platform.target }}
|
||||
path: .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz
|
||||
|
||||
1
.github/workflows/stale.yml
vendored
1
.github/workflows/stale.yml
vendored
@@ -21,3 +21,4 @@ jobs:
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
exempt-issue-labels: "backlog"
|
||||
exempt-all-milestones: true
|
||||
|
||||
6
.github/workflows/website.yml
vendored
6
.github/workflows/website.yml
vendored
@@ -33,11 +33,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v2
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "./site/"
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -1,6 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
- [Changelog](#changelog)
|
||||
- [0.18.0](#0180)
|
||||
- [0.17.0](#0170)
|
||||
- [0.16.1](#0161)
|
||||
- [0.16.0](#0160)
|
||||
- [0.15.0](#0150)
|
||||
@@ -39,6 +41,47 @@
|
||||
|
||||
---
|
||||
|
||||
## 0.18.0
|
||||
|
||||
Released on 10/06/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
|
||||
|
||||
Released on 24/03/2025
|
||||
|
||||
- **Queuing transfers**:
|
||||
- the logic of selecting files has been extended!
|
||||
- From now on selecting file will put the files into a **transfer queue**, which is shown on the bottom panel.
|
||||
- When a file is selected the file is added to the queue with a destination path, which is the **current other explorer path at the moment of selection.**
|
||||
- It is possible to navigate to the transfer queue by using `P` and pressing `ENTER` or `DELETE` on a file will remove it from the transfer queue.
|
||||
- Other commands will work as well on the transfer queue, like `COPY`, `MOVE`, `DELETE`, `RENAME`.
|
||||
- [issue 308](https://github.com/veeso/termscp/issues/308): added `--wno-keyring` flag to disable keyring
|
||||
- [issue 316](https://github.com/veeso/termscp/issues/316): Local directory path is not switching to what's specified in the bookmark. Now the local directory path is correctly set following this hierarchy:
|
||||
1. Local directory path specified for the host bridge
|
||||
2. Local directory path specified in the bookmark
|
||||
3. Working directory
|
||||
- [issue 317](https://github.com/veeso/termscp/issues/317): the return value of `--version` should be `0`
|
||||
- [issue 319](https://github.com/veeso/termscp/issues/319): fixed a crash when the local directory specified in the auth form does not exist
|
||||
- [issue 327](https://github.com/veeso/termscp/issues/327): fixed a panic when trying to go up from local directory on localhost in the auth form
|
||||
- [issue 330](https://github.com/veeso/termscp/issues/330): add suppaftp/pavao/kube to allowed logs
|
||||
- Dependencies:
|
||||
- `argh` to `0.1.13`
|
||||
- `bytesize` to `2`
|
||||
- `dirs` to `6`
|
||||
- `magic-crypt` to `4`
|
||||
- `notify` to `8`
|
||||
- `ssh2-config` to `0.4`
|
||||
- `remotefs-ssh` to `0.6`
|
||||
- `rust` edition to `2024`
|
||||
|
||||
## 0.16.1
|
||||
|
||||
Released on 12/11/2024
|
||||
|
||||
@@ -2,75 +2,131 @@
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at christian.visintin1997@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[INSERT CONTACT METHOD].
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
<https://www.contributor-covenant.org/faq>
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Contributing
|
||||
|
||||
Before contributing to this repository, please first discuss the change you wish to make via issue of this repository before making a change.
|
||||
Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
|
||||
Please note we have a [code of conduct](CODE_OF_CONDUCT.md). Please follow it in all your interactions with the project.
|
||||
|
||||
- [Contributing](#contributing)
|
||||
- [Project mission](#project-mission)
|
||||
@@ -20,9 +20,13 @@ Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in
|
||||
|
||||
## Project mission
|
||||
|
||||
termscp was born because, as a terminal lover and Linux user, I wanted something like WinSCP on Linux and on terminal. I my previous job I used SFTP/SCP pratically everyday and that made me to desire an application like termscp so much, that eventually I started to work on it in the spare time. I saw there was a very cool library to create terminal user interface (`tui-rs`), so I started to code it. I wrote termscp as an experiment, I designed kinda nothing at the time. I just said
|
||||
termscp was born because, as a terminal lover and Linux user, I wanted something like WinSCP on Linux and on terminal. At my previous job, I used SFTP/SCP practically everyday and that made me desire an application like termscp so much that eventually I started to work on it in my spare time.
|
||||
|
||||
> Ok, there must be a `FileTransfer` trait somehow, I'll have more views, so I'll use something like Android activities, and there must be a module to interact with the local host".
|
||||
I saw there was a very cool library to create terminal user interfaces (`tui-rs`, now `ratatui`), so I started to code it.
|
||||
|
||||
I wrote termscp as an experiment. I didn't design anything at the time. I just said,
|
||||
|
||||
> "Ok, there must be a FileTransfer trait somehow. I'll have more views, so I'll use something like Android activities, and there must be a module to interact with the local host."
|
||||
|
||||
And so in december 2020 I had the first version of termscp running and it worked, but was very simple, raw and minimal.
|
||||
A lot of things have changed since them, both the features the project provides and my personal view of this project.
|
||||
|
||||
3294
Cargo.lock
generated
3294
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
78
Cargo.toml
78
Cargo.toml
@@ -2,7 +2,7 @@
|
||||
authors = ["Christian Visintin <christian.visintin@veeso.dev>"]
|
||||
categories = ["command-line-utilities"]
|
||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
homepage = "https://termscp.veeso.dev"
|
||||
include = ["src/**/*", "build.rs", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
|
||||
@@ -10,7 +10,8 @@ license = "MIT"
|
||||
name = "termscp"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
version = "0.16.1"
|
||||
version = "0.18.0"
|
||||
rust-version = "1.85.1"
|
||||
|
||||
[package.metadata.rpm]
|
||||
package = "termscp"
|
||||
@@ -23,7 +24,7 @@ termscp = { path = "/usr/bin/termscp" }
|
||||
|
||||
[package.metadata.deb]
|
||||
maintainer = "Christian Visintin <christian.visintin@veeso.dev>"
|
||||
copyright = "2022, Christian Visintin <christian.visintin@veeso.dev>"
|
||||
copyright = "2025, Christian Visintin <christian.visintin@veeso.dev>"
|
||||
extended-description-file = "docs/misc/README.deb.txt"
|
||||
|
||||
[[bin]]
|
||||
@@ -33,37 +34,35 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
argh = "^0.1"
|
||||
bitflags = "^2"
|
||||
bytesize = "^1"
|
||||
bytesize = "^2"
|
||||
chrono = "^0.4"
|
||||
content_inspector = "^0.2"
|
||||
dirs = "^5.0"
|
||||
dirs = "^6"
|
||||
edit = "^0.1"
|
||||
filetime = "^0.2"
|
||||
hostname = "^0.4"
|
||||
keyring = { version = "^3", optional = true, features = [
|
||||
keyring = { version = "^3", features = [
|
||||
"apple-native",
|
||||
"windows-native",
|
||||
"sync-secret-service",
|
||||
"vendored",
|
||||
] }
|
||||
lazy-regex = "^3"
|
||||
lazy_static = "^1"
|
||||
log = "^0.4"
|
||||
magic-crypt = "^3"
|
||||
notify = "6"
|
||||
notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
|
||||
magic-crypt = "4"
|
||||
notify = "8"
|
||||
notify-rust = { version = "^4", default-features = false, features = ["d"] }
|
||||
nucleo = "0.5"
|
||||
open = "^5.0"
|
||||
rand = "^0.8.5"
|
||||
rand = "^0.9"
|
||||
regex = "^1"
|
||||
remotefs = "^0.3"
|
||||
remotefs-aws-s3 = { version = "^0.3", default-features = false, features = [
|
||||
"find",
|
||||
"rustls",
|
||||
] }
|
||||
remotefs-aws-s3 = "0.4"
|
||||
remotefs-kube = "0.4"
|
||||
remotefs-webdav = "^0.2"
|
||||
rpassword = "^7"
|
||||
self_update = { version = "^0.41", default-features = false, features = [
|
||||
self_update = { version = "^0.42", default-features = false, features = [
|
||||
"rustls",
|
||||
"archive-tar",
|
||||
"archive-zip",
|
||||
@@ -72,18 +71,31 @@ self_update = { version = "^0.41", default-features = false, features = [
|
||||
] }
|
||||
serde = { version = "^1", features = ["derive"] }
|
||||
simplelog = "^0.12"
|
||||
ssh2-config = "^0.2"
|
||||
tempfile = "^3"
|
||||
thiserror = "^1"
|
||||
tokio = { version = "=1.38.1", features = ["rt"] }
|
||||
ssh2-config = "^0.5"
|
||||
tempfile = "3"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1.44", features = ["rt"] }
|
||||
toml = "^0.8"
|
||||
tui-realm-stdlib = "2"
|
||||
tuirealm = "2"
|
||||
tui-realm-stdlib = "3"
|
||||
tuirealm = "3"
|
||||
tui-term = "0.2"
|
||||
unicode-width = "^0.2"
|
||||
version-compare = "^0.2"
|
||||
whoami = "^1.5"
|
||||
whoami = "^1.6"
|
||||
wildmatch = "^2"
|
||||
|
||||
[target."cfg(not(target_os = \"macos\"))".dependencies]
|
||||
remotefs-smb = { version = "^0.3", optional = true }
|
||||
|
||||
[target."cfg(target_family = \"unix\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
|
||||
remotefs-ssh = { version = "^0.6", features = ["ssh2-vendored"] }
|
||||
uzers = "0.12"
|
||||
|
||||
[target."cfg(target_family = \"windows\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.2", features = ["native-tls"] }
|
||||
remotefs-ssh = { version = "^0.6" }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "^1"
|
||||
serial_test = "^3"
|
||||
@@ -92,27 +104,13 @@ serial_test = "^3"
|
||||
cfg_aliases = "0.2"
|
||||
vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] }
|
||||
|
||||
|
||||
[features]
|
||||
default = ["smb", "with-keyring"]
|
||||
default = ["smb", "keyring"]
|
||||
github-actions = []
|
||||
isolated-tests = []
|
||||
smb = ["remotefs-smb"]
|
||||
with-keyring = ["keyring"]
|
||||
|
||||
[target."cfg(not(target_os = \"macos\"))".dependencies]
|
||||
remotefs-smb = { version = "^0.3", optional = true }
|
||||
|
||||
[target."cfg(target_family = \"windows\")"]
|
||||
[target."cfg(target_family = \"windows\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.2", features = ["native-tls"] }
|
||||
remotefs-ssh = "^0.4"
|
||||
|
||||
[target."cfg(target_family = \"unix\")"]
|
||||
[target."cfg(target_family = \"unix\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
|
||||
remotefs-ssh = { version = "^0.4", features = ["ssh2-vendored"] }
|
||||
uzers = "0.12"
|
||||
keyring = []
|
||||
smb = ["dep:remotefs-smb"]
|
||||
smb-vendored = ["remotefs-smb/vendored"]
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="/docs/ptbr/README.md"
|
||||
href="/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
@@ -70,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Developed by <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Current version: 0.16.1 12/11/2024</p>
|
||||
<p align="center">Developed by <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Current version: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<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
|
||||
- 💁 SFTP/SCP authentication with SSH keys and username/password
|
||||
- 🐧 Compatible with Windows, Linux, FreeBSD, NetBSD and MacOS
|
||||
- 🐚 Embedded terminal for executing commands on the system.
|
||||
- 🎨 Make it yours!
|
||||
- Themes
|
||||
- Custom file explorer format
|
||||
|
||||
28
dist/build/aarch64_debian10/Dockerfile
vendored
28
dist/build/aarch64_debian10/Dockerfile
vendored
@@ -9,9 +9,33 @@ RUN apt update && apt install -y \
|
||||
libdbus-1-dev \
|
||||
build-essential \
|
||||
libsmbclient-dev \
|
||||
libsmbclient \
|
||||
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 \
|
||||
bash \
|
||||
curl
|
||||
curl \
|
||||
cpanminus && \
|
||||
cpanm Parse::Yapp::Driver;
|
||||
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
|
||||
2
dist/build/linux-aarch64.sh
vendored
2
dist/build/linux-aarch64.sh
vendored
@@ -38,7 +38,7 @@ cd -
|
||||
mkdir -p ${PKGS_DIR}/deb/
|
||||
mkdir -p ${PKGS_DIR}/aarch64-unknown-linux-gnu/
|
||||
docker run --name "$ARM64_DEB_NAME" -d "$ARM64_DEB_NAME" || docker start "$ARM64_DEB_NAME"
|
||||
docker exec -it "$ARM64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release && cargo deb"
|
||||
docker exec -it "$ARM64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release --features smb-vendored && cargo deb"
|
||||
docker cp ${ARM64_DEB_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}-1_arm64.deb ${PKGS_DIR}/deb/termscp_${VERSION}_arm64.deb
|
||||
docker cp ${ARM64_DEB_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/aarch64-unknown-linux-gnu/
|
||||
docker stop "$ARM64_DEB_NAME"
|
||||
|
||||
4
dist/build/linux-x86_64.sh
vendored
4
dist/build/linux-x86_64.sh
vendored
@@ -30,13 +30,13 @@ PKGS_DIR=$(pwd)/pkgs
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/
|
||||
# Build x86_64_deb
|
||||
cd x86_64_debian10/
|
||||
cd x86_64_debian12/
|
||||
docker build $CACHE --build-arg branch=${BRANCH} --tag "$X86_64_DEB_NAME" .
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/deb/
|
||||
mkdir -p ${PKGS_DIR}/x86_64-unknown-linux-gnu/
|
||||
docker run --name "$X86_64_DEB_NAME" -d "$X86_64_DEB_NAME" || docker start "$X86_64_DEB_NAME"
|
||||
docker exec -it "$X86_64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release && cargo deb"
|
||||
docker exec -it "$X86_64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release --features smb-vendored && cargo deb"
|
||||
docker cp ${X86_64_DEB_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}-1_amd64.deb ${PKGS_DIR}/deb/termscp_${VERSION}_amd64.deb
|
||||
docker cp ${X86_64_DEB_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/x86_64-unknown-linux-gnu/
|
||||
docker stop "$X86_64_DEB_NAME"
|
||||
|
||||
4
dist/build/macos.sh
vendored
4
dist/build/macos.sh
vendored
@@ -81,7 +81,7 @@ fi
|
||||
# Build release (x86_64)
|
||||
X86_TARGET=""
|
||||
X86_TARGET_DIR=""
|
||||
if [ "$ARCH" = "aarch64" ]; then
|
||||
if [ "$ARCH" = "x86_64" ]; then
|
||||
X86_TARGET="--target x86_64-apple-darwin"
|
||||
X86_TARGET_DIR="target/x86_64-apple-darwin/release/"
|
||||
fi
|
||||
@@ -92,7 +92,7 @@ RET_X86_64=$?
|
||||
|
||||
ARM64_TARGET=""
|
||||
ARM64_TARGET_DIR=""
|
||||
if [ "$ARCH" = "aarch64" ]; then
|
||||
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
||||
ARM64_TARGET="--target aarch64-apple-darwin"
|
||||
ARM64_TARGET_DIR="target/aarch64-apple-darwin/release/"
|
||||
fi
|
||||
|
||||
27
dist/build/x86_64_debian10/Dockerfile
vendored
27
dist/build/x86_64_debian10/Dockerfile
vendored
@@ -1,27 +0,0 @@
|
||||
FROM debian:buster
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y \
|
||||
git \
|
||||
gcc \
|
||||
pkg-config \
|
||||
libdbus-1-dev \
|
||||
build-essential \
|
||||
libsmbclient-dev \
|
||||
libsmbclient \
|
||||
bash \
|
||||
curl
|
||||
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
chmod +x /tmp/rust.sh && \
|
||||
/tmp/rust.sh -y
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/veeso/termscp.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/termscp/
|
||||
# Install cargo deb
|
||||
RUN . $HOME/.cargo/env && cargo install cargo-deb
|
||||
|
||||
ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
54
dist/build/x86_64_debian12/Dockerfile
vendored
Normal file
54
dist/build/x86_64_debian12/Dockerfile
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
FROM debian:bookworm
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y \
|
||||
git \
|
||||
gcc \
|
||||
pkg-config \
|
||||
libdbus-1-dev \
|
||||
build-essential \
|
||||
libsmbclient-dev \
|
||||
libgit2-dev \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libbsd-dev \
|
||||
libcap-dev \
|
||||
libcups2-dev \
|
||||
libgnutls28-dev \
|
||||
libgnutls30 \
|
||||
libicu-dev \
|
||||
libjansson-dev \
|
||||
libkeyutils-dev \
|
||||
libldap2-dev \
|
||||
zlib1g-dev \
|
||||
libpam0g-dev \
|
||||
libacl1-dev \
|
||||
libarchive-dev \
|
||||
libssl-dev \
|
||||
flex \
|
||||
bison \
|
||||
libntirpc-dev \
|
||||
libglib2.0-dev \
|
||||
libdbus-1-dev \
|
||||
libsasl2-dev \
|
||||
libunistring-dev \
|
||||
bash \
|
||||
curl \
|
||||
cpanminus && \
|
||||
cpanm Parse::Yapp::Driver;
|
||||
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
chmod +x /tmp/rust.sh && \
|
||||
/tmp/rust.sh -y && \
|
||||
. $HOME/.cargo/env && \
|
||||
cargo version
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/veeso/termscp.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/termscp/
|
||||
# Install cargo deb
|
||||
RUN . $HOME/.cargo/env && cargo install cargo-deb
|
||||
|
||||
ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
@@ -22,7 +22,7 @@
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
@@ -70,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Entwickelt von <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Aktuelle Version: 0.16.1 12/11/2024</p>
|
||||
<p align="center">Entwickelt von <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Aktuelle Version: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- [Dateiexplorer 📂](#dateiexplorer-)
|
||||
- [Tastenkombinationen ⌨](#tastenkombinationen-)
|
||||
- [Mit mehreren Dateien arbeiten 🥷](#mit-mehreren-dateien-arbeiten-)
|
||||
- [Beispiel](#beispiel)
|
||||
- [Synchronisiertes Durchsuchen ⏲️](#synchronisiertes-durchsuchen-️)
|
||||
- [Öffnen und Öffnen mit 🚪](#öffnen-und-öffnen-mit-)
|
||||
- [Lesezeichen ⭐](#lesezeichen-)
|
||||
@@ -297,19 +298,34 @@ Diese Panels sind im Wesentlichen 3 (ja, tatsächlich drei):
|
||||
| <CTRL+C> | Dateiübertragungsvorgang abbrechen | |
|
||||
| <CTRL+T> | Alle synchronisierten Pfade anzeigen | Track |
|
||||
|
||||
### Mit mehreren Dateien arbeiten 🥷
|
||||
### Mit mehreren Dateien arbeiten 🥷
|
||||
|
||||
Sie können mit mehreren Dateien arbeiten, indem Sie `<M>` drücken, um die aktuelle Datei auszuwählen, oder `<CTRL+A>`, um alle Dateien im Arbeitsverzeichnis auszuwählen.
|
||||
Sobald eine Datei zur Auswahl markiert ist, wird sie mit einem `*` auf der linken Seite angezeigt.
|
||||
Bei der Arbeit mit der Auswahl werden nur die ausgewählten Dateien für Aktionen verarbeitet, während der aktuell hervorgehobene Eintrag ignoriert wird.
|
||||
Es ist auch möglich, mit mehreren Dateien im Suchergebnis-Panel zu arbeiten.
|
||||
Alle Aktionen sind verfügbar, wenn Sie mit mehreren Dateien arbeiten, aber beachten Sie, dass einige Aktionen etwas anders funktionieren. Schauen wir uns das genauer an:
|
||||
Du kannst mit mehreren Dateien gleichzeitig arbeiten, mit diesen einfachen Tastenkombinationen:
|
||||
|
||||
- _Kopieren_: Wann immer Sie eine Datei kopieren, werden Sie aufgefordert, den Zielnamen einzugeben. Bei der Arbeit mit mehreren Dateien bezieht sich dieser Name auf das Zielverzeichnis, in dem alle diese Dateien kopiert werden.
|
||||
- `<M>`: Datei zur Auswahl markieren
|
||||
- `<CTRL+A>`: alle Dateien im aktuellen Verzeichnis auswählenas
|
||||
- `<ALT+A>`: Auswahl aller Dateien aufheben
|
||||
|
||||
- _Umbenennen_: Dasselbe wie Kopieren, aber die Dateien werden dorthin verschoben.
|
||||
Markierte Dateien werden **mit hervorgehobenem Hintergrund** angezeigt.
|
||||
Bei Auswahlaktionen werden nur die markierten Dateien verarbeitet, das aktuell hervorgehobene Element wird ignoriert.
|
||||
|
||||
- _Speichern unter_: Dasselbe wie Kopieren, aber die Dateien werden dorthin geschrieben.
|
||||
Auch im Suchergebnis-Panel ist die Mehrfachauswahl möglich.
|
||||
|
||||
Alle Aktionen sind bei mehreren Dateien verfügbar, einige funktionieren jedoch leicht anders:
|
||||
|
||||
- *Kopieren*: du wirst nach einem Zielnamen gefragt. Bei mehreren Dateien ist das das Zielverzeichnis.
|
||||
- *Umbenennen*: wie Kopieren, aber verschiebt die Dateien.
|
||||
- *Speichern unter*: wie Kopieren, aber schreibt die Dateien dorthin.
|
||||
|
||||
Wenn du eine Datei in einem Verzeichnis (z. B. `/home`) auswählst und dann das Verzeichnis wechselst, bleibt sie ausgewählt und erscheint in der **Transfer-Warteschlange** im unteren Panel.
|
||||
Beim Markieren einer Datei wird das aktuelle *Remote*-Verzeichnis gespeichert; bei einem Transfer wird sie in dieses Verzeichnis übertragen.
|
||||
|
||||
#### Beispiel
|
||||
|
||||
Wenn wir `/home/a.txt` lokal auswählen und im Remote-Panel in `/tmp` sind, dann zu `/var` wechseln, `/var/b.txt` auswählen und im Remote-Panel in `/home` sind, ergibt der Transfer:
|
||||
|
||||
- `/home/a.txt` → `/tmp/a.txt`
|
||||
- `/var/b.txt` → `/home/b.txt`
|
||||
|
||||
### Synchronisiertes Durchsuchen ⏲️
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
@@ -70,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Desarrollado por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versión actual: 0.16.1 12/11/2024</p>
|
||||
<p align="center">Desarrollado por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versión actual: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
- [Credenciales de S3 🦊](#credenciales-de-s3-)
|
||||
- [Explorador de archivos 📂](#explorador-de-archivos-)
|
||||
- [Keybindings ⌨](#keybindings-)
|
||||
- [Trabaja en varios archivos 🥷](#trabaja-en-varios-archivos-)
|
||||
- [Trabajar con múltiples archivos 🥷](#trabajar-con-múltiples-archivos-)
|
||||
- [Ejemplo](#ejemplo)
|
||||
- [Navegación sincronizada ⏲️](#navegación-sincronizada-️)
|
||||
- [Abierta y abierta con 🚪](#abierta-y-abierta-con-)
|
||||
- [Marcadores ⭐](#marcadores-)
|
||||
@@ -259,17 +260,34 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
|
||||
| `<CTRL+C>` | Abortar el proceso de transferencia de archivos | |
|
||||
| `<CTRL+T>` | Mostrar todas las rutas sincronizadas | Track |
|
||||
|
||||
### Trabaja en varios archivos 🥷
|
||||
### Trabajar con múltiples archivos 🥷
|
||||
|
||||
Puede optar por trabajar en varios archivos, seleccionándolos presionando `<M>`, para seleccionar el archivo actual, o presionando `<CTRL + A>`, que seleccionará todos los archivos en el directorio de trabajo.
|
||||
Una vez que un archivo está marcado para su selección, se mostrará con un `*` a la izquierda.
|
||||
Al trabajar en la selección, solo se procesará el archivo seleccionado para las acciones, mientras que el elemento resaltado actual se ignorará.
|
||||
También es posible trabajar en varios archivos desde el panel de resultados de búsqueda.
|
||||
Todas las acciones están disponibles cuando se trabaja con varios archivos, pero tenga en cuenta que algunas acciones funcionan de forma ligeramente diferente. Vamos a sumergirnos en:
|
||||
Puedes optar por trabajar con varios archivos, usando estos controles:
|
||||
|
||||
- *Copy*: cada vez que copie un archivo, se le pedirá que inserte el nombre de destino. Cuando se trabaja con varios archivos, este nombre se refiere al directorio de destino donde se copiarán todos estos archivos.
|
||||
- *Rename*: igual que copiar, pero moverá archivos allí.
|
||||
- *Save as*: igual que copiar, pero los escribirá allí.
|
||||
- `<M>`: marcar un archivo para selección
|
||||
- `<CTRL+A>`: seleccionar todos los archivos del directorio actual
|
||||
- `<ALT+A>`: deseleccionar todos los archivos
|
||||
|
||||
Una vez marcado, el archivo será **mostrado con un fondo resaltado** .
|
||||
Cuando se trabaja con una selección, solo los archivos seleccionados serán procesados; el archivo resaltado actual será ignorado.
|
||||
|
||||
También se puede trabajar con múltiples archivos desde el panel de resultados de búsqueda.
|
||||
|
||||
Todas las acciones están disponibles con archivos múltiples, pero algunas funcionan de forma algo distinta. Veamos:
|
||||
|
||||
- *Copiar*: al copiar, se pedirá el nombre de destino. Para varios archivos, es el directorio donde se copiarán.
|
||||
- *Renombrar*: igual que copiar, pero mueve los archivos.
|
||||
- *Guardar como*: igual que copiar, pero escribe los archivos allí.
|
||||
|
||||
Si seleccionas un archivo en un directorio (ej. `/home`) y cambias de directorio, seguirá seleccionado y se mostrará en la **cola de transferencia** en el panel inferior.
|
||||
Cuando se selecciona un archivo, se asocia la carpeta *remota* actual con él; si se transfiere, será a esa carpeta.
|
||||
|
||||
#### Ejemplo
|
||||
|
||||
Si seleccionamos `/home/a.txt` localmente y estamos en `/tmp` en remoto, luego cambiamos a `/var`, seleccionamos `/var/b.txt` y estamos en `/home` en el panel remoto, el resultado de transferir será:
|
||||
|
||||
- `/home/a.txt` transferido a `/tmp/a.txt`
|
||||
- `/var/b.txt` transferido a `/home/b.txt`
|
||||
|
||||
### Navegación sincronizada ⏲️
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
@@ -70,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Développé par <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Version actuelle: 0.16.1 12/11/2024</p>
|
||||
<p align="center">Développé par <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Version actuelle: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
|
||||
- [Raccourcis clavier ⌨](#raccourcis-clavier-)
|
||||
- [Travailler sur plusieurs fichiers 🥷](#travailler-sur-plusieurs-fichiers-)
|
||||
- [Exemple](#exemple)
|
||||
- [Navigation synchronisée ⏲️](#navigation-synchronisée-️)
|
||||
- [Ouvrir et ouvrir avec 🚪](#ouvrir-et-ouvrir-avec-)
|
||||
- [Signets ⭐](#signets-)
|
||||
@@ -258,17 +259,34 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
|
||||
| `<CTRL+C>` | Abandonner le processus de transfert de fichiers | |
|
||||
| `<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, en les sélectionnant en appuyant sur `<M>`, afin de sélectionner le fichier actuel, ou en appuyant sur `<CTRL+A>`, ce qui sélectionnera tous les fichiers dans le répertoire de travail.
|
||||
Une fois qu'un fichier est marqué pour la sélection, il sera affiché avec un `*` sur la gauche.
|
||||
Lorsque vous travaillez sur la sélection, seul le fichier sélectionné sera traité pour les actions, tandis que l'élément en surbrillance actuel sera ignoré.
|
||||
Il est également possible de travailler sur plusieurs fichiers dans le panneau des résultats de recherche.
|
||||
Toutes les actions sont disponibles lorsque vous travaillez avec plusieurs fichiers, mais sachez que certaines actions fonctionnent de manière légèrement différente. Plongeons dans:
|
||||
Vous pouvez choisir de travailler sur plusieurs fichiers avec ces simples commandes :
|
||||
|
||||
- *Copy*: chaque fois que vous copiez un fichier, vous serez invité à insérer le nom de destination. Lorsque vous travaillez avec plusieurs fichiers, ce nom fait référence au répertoire de destination où tous ces fichiers seront copiés.
|
||||
- *Rename*: identique à la copie, mais y déplacera les fichiers.
|
||||
- *Save as*: identique à la copie, mais les y écrira.
|
||||
- `<M>` : marquer un fichier à sélectionner
|
||||
- `<CTRL+A>` : sélectionner tous les fichiers du répertoire actuel
|
||||
- `<ALT+A>` : désélectionner tous les fichiers
|
||||
|
||||
Une fois sélectionné, un fichier sera **affiché avec un fond en surbrillance** .
|
||||
Lorsqu’on travaille avec des sélections, seules les fichiers sélectionnés seront affectés par les actions, tandis que l'élément actuellement surligné sera ignoré.
|
||||
|
||||
Il est également possible de travailler avec plusieurs fichiers depuis le panneau des résultats de recherche.
|
||||
|
||||
Toutes les actions sont disponibles avec des fichiers multiples, mais certaines peuvent se comporter différemment. Détails :
|
||||
|
||||
- *Copier* : lors de la copie, il vous sera demandé un nom de destination. Avec plusieurs fichiers, cela correspond au dossier de destination.
|
||||
- *Renommer* : identique à la copie, mais déplace les fichiers.
|
||||
- *Enregistrer sous* : identique à la copie, mais enregistre les fichiers à cet emplacement.
|
||||
|
||||
Si vous sélectionnez un fichier dans un dossier (ex. `/home`) puis changez de répertoire, il restera sélectionné et sera affiché dans la **file d’attente de transfert** en bas.
|
||||
Lorsqu’un fichier est sélectionné, le dossier *distant* courant lui est associé ; en cas de transfert, il sera envoyé vers ce dossier.
|
||||
|
||||
#### Exemple
|
||||
|
||||
Si on sélectionne `/home/a.txt` localement et que le panneau distant est sur `/tmp`, puis on passe à `/var`, on sélectionne `/var/b.txt` et que le panneau distant est sur `/home`, le transfert donnera :
|
||||
|
||||
- `/home/a.txt` transféré vers `/tmp/a.txt`
|
||||
- `/var/b.txt` transféré vers `/home/b.txt`
|
||||
|
||||
### Navigation synchronisée ⏲️
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
@@ -70,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Sviluppato da <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versione corrente: 0.16.1 12/11/2024</p>
|
||||
<p align="center">Sviluppato da <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versione corrente: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
- [Credenziali S3 🦊](#credenziali-s3-)
|
||||
- [File explorer 📂](#file-explorer-)
|
||||
- [Abbinamento tasti ⌨](#abbinamento-tasti-)
|
||||
- [Lavora su più file 🥷](#lavora-su-più-file-)
|
||||
- [Lavora con più file 🥷](#lavora-con-più-file-)
|
||||
- [Esempio](#esempio)
|
||||
- [Synchronized browsing ⏲️](#synchronized-browsing-️)
|
||||
- [Apri e apri con 🚪](#apri-e-apri-con-)
|
||||
- [Segnalibri ⭐](#segnalibri-)
|
||||
@@ -254,17 +255,34 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
|
||||
| `<CTRL+C>` | Annulla trasferimento file | |
|
||||
| `<CTRL+T>` | Visualizza tutti i percorsi sincronizzati | Track |
|
||||
|
||||
### Lavora su più file 🥷
|
||||
### Lavora con più file 🥷
|
||||
|
||||
Puoi lavorare su una selezione di file, marcandoli come selezionati tramite `<M>`, per selezionare il file corrente o con `<CTRL+A` per selezionarli tutti.
|
||||
Una volta che un file è marcato, sarà visualizzato con un `*` prima del nome.
|
||||
Quando lavori con una selezioni, solo i file selezionati saranno presi in considerazione (l'eventuale file evidenziato sarà ignorato).
|
||||
È possibile operare su più file anche nel pannello di ricerca.
|
||||
Tutte le azioni sono disponibili quando si lavora sulle selezioni, ma occhio, che alcune azioni si comporteranno in maniera leggermente differente. Vediamo quali e come:
|
||||
Puoi scegliere di lavorare con più file, usando questi semplici comandi:
|
||||
|
||||
- *Copia*: Se copi un file, ti verrà richiesto di inserire il nome delle destinazione, ma quando lavori con la selezione, il nome si riferisce alla directory di destinazione, mentre il nome del file rimarrà inviariato.
|
||||
- *Rinomina*: Come il copia, ma li sposterà.
|
||||
- *Salva con nome*: Come il copia, ma li trasferirà.
|
||||
- `<M>`: marca un file per la selezione
|
||||
- `<CTRL+A>`: seleziona tutti i file nella directory corrente
|
||||
- `<ALT+A>`: deseleziona tutti i file
|
||||
|
||||
Una volta che un file è stato selezionato, verrà **evidenziato con uno sfondo colorato** .
|
||||
Quando lavori su una selezione, solo i file selezionati verranno processati per le azioni, mentre l'elemento attualmente evidenziato sarà ignorato.
|
||||
|
||||
È possibile lavorare con più file anche dal pannello dei risultati di ricerca.
|
||||
|
||||
Tutte le azioni sono disponibili anche quando si lavora con più file, ma alcune funzionano in modo leggermente diverso. Ecco i dettagli:
|
||||
|
||||
- *Copia*: quando copi un file, ti verrà chiesto di inserire il nome di destinazione. Con più file selezionati, questo nome rappresenta la cartella di destinazione dove verranno copiati.
|
||||
- *Rinomina*: come la copia, ma i file verranno spostati lì.
|
||||
- *Salva come*: come la copia, ma i file verranno salvati lì.
|
||||
|
||||
Se selezioni un file in una directory (es. `/home`) e poi cambi directory, il file rimarrà selezionato e sarà visibile nella **coda di trasferimento** nel pannello inferiore.
|
||||
Quando un file viene selezionato, la directory *remota* corrente viene associata all’elemento; quindi, se il file viene trasferito, verrà trasferito nella directory associata.
|
||||
|
||||
#### Esempio
|
||||
|
||||
Se selezioniamo un file locale `/home/a.txt`, siamo su `/tmp` nel pannello remoto, poi ci spostiamo su `/var`, selezioniamo `/var/b.txt`, e sul pannello remoto siamo su `/home`, eseguendo il trasferimento otterremo:
|
||||
|
||||
- `/home/a.txt` trasferito su `/tmp/a.txt`
|
||||
- `/var/b.txt` trasferito su `/home/b.txt`
|
||||
|
||||
### Synchronized browsing ⏲️
|
||||
|
||||
|
||||
23
docs/man.md
23
docs/man.md
@@ -16,6 +16,7 @@
|
||||
- [File explorer 📂](#file-explorer-)
|
||||
- [Keybindings ⌨](#keybindings-)
|
||||
- [Work on multiple files 🥷](#work-on-multiple-files-)
|
||||
- [Example](#example)
|
||||
- [Synchronized browsing ⏲️](#synchronized-browsing-️)
|
||||
- [Open and Open With 🚪](#open-and-open-with-)
|
||||
- [Bookmarks ⭐](#bookmarks-)
|
||||
@@ -274,16 +275,34 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
|
||||
|
||||
### Work on multiple files 🥷
|
||||
|
||||
You can opt to work on multiple files, selecting them pressing `<M>`, in order to select the current file, or pressing `<CTRL+A>`, which will select all the files in the working directory.
|
||||
Once a file is marked for selection, it will be displayed with a `*` on the left.
|
||||
You can opt to work on multiple files, with these simple controls:
|
||||
|
||||
- `<M>`: mark a file for selection
|
||||
- `<CTRL+A>`: select all files in the current directory
|
||||
- `<ALT+A>`: deselect all files
|
||||
|
||||
Once a file is marked for selection, it will be **displayed with an highlighted background**.
|
||||
|
||||
When working on selection, only selected file will be processed for actions, while the current highlighted item will be ignored.
|
||||
It is possible to work on multiple files also when in the find result panel.
|
||||
|
||||
All the actions are available when working with multiple files, but be aware that some actions work in a slightly different way. Let's dive in:
|
||||
|
||||
- *Copy*: whenever you copy a file, you'll be prompted to insert the destination name. When working with multiple file, this name refers to the destination directory where all these files will be copied.
|
||||
- *Rename*: same as copy, but will move files there.
|
||||
- *Save as*: same as copy, but will write them there.
|
||||
|
||||
If you select a file in a directory (e.g. `/home`) and then you change directory the file will be kept selected and it will be displayed in the **transfer queue** in the bottom panel.
|
||||
|
||||
When a file gets selected the current *remote* directory is associated to its entry; so in case the file gets transferred it will be transferred to the directory associated to the file.
|
||||
|
||||
#### Example
|
||||
|
||||
If we select a file on local `/home/a.txt` and we're currently at `/tmp` on remote and then we move to `/var` and we select `/var/b.txt` and on the remote panel we're at `/home` and we perform a transfer the result will be:
|
||||
|
||||
- `/home/a.txt` transferred to `/tmp/a.txt`
|
||||
- `/var/b.txt` transferred to `/home/b.txt`
|
||||
|
||||
### Synchronized browsing ⏲️
|
||||
|
||||
When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels.
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
@@ -70,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Desenvolvido por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versão atual: 0.16.1 12/11/2024</p>
|
||||
<p align="center">Desenvolvido por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versão atual: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -15,7 +15,8 @@
|
||||
- [Credenciais do S3 🦊](#credenciais-do-s3-)
|
||||
- [Explorador de Arquivos 📂](#explorador-de-arquivos-)
|
||||
- [Atalhos de Teclado ⌨](#atalhos-de-teclado-)
|
||||
- [Trabalhar com Vários Arquivos 🥷](#trabalhar-com-vários-arquivos-)
|
||||
- [Trabalhar com múltiplos arquivos 🥷](#trabalhar-com-múltiplos-arquivos-)
|
||||
- [Exemplo](#exemplo)
|
||||
- [Navegação Sincronizada ⏲️](#navegação-sincronizada-️)
|
||||
- [Abrir e Abrir Com 🚪](#abrir-e-abrir-com-)
|
||||
- [Favoritos ⭐](#favoritos-)
|
||||
@@ -272,17 +273,34 @@ Para trocar de painel, você precisa pressionar `<LEFT>` para mover para o paine
|
||||
| `<CTRL+C>` | Abortir processo de transferência de arquivo | |
|
||||
| `<CTRL+T>` | Mostrar todos os caminhos sincronizados | Track |
|
||||
|
||||
### Trabalhar com Vários Arquivos 🥷
|
||||
### Trabalhar com múltiplos arquivos 🥷
|
||||
|
||||
Você pode optar por trabalhar com vários arquivos, selecionando-os pressionando `<M>`, para selecionar o arquivo atual, ou pressionando `<CTRL+A>`, que selecionará todos os arquivos no diretório de trabalho.
|
||||
Uma vez que um arquivo esteja marcado para seleção, ele será exibido com um `*` à esquerda.
|
||||
Ao trabalhar com seleção, apenas o arquivo selecionado será processado para ações, enquanto o item destacado atual será ignorado.
|
||||
É possível trabalhar com vários arquivos também quando estiver no painel de resultados da busca.
|
||||
Todas as ações estão disponíveis ao trabalhar com vários arquivos, mas tenha em mente que algumas ações funcionam de forma ligeiramente diferente. Vamos explicar algumas delas:
|
||||
Você pode optar por trabalhar com vários arquivos, usando estes controles simples:
|
||||
|
||||
- *Copiar*: sempre que você copiar um arquivo, você será solicitado a inserir o nome de destino. Ao trabalhar com vários arquivos, esse nome refere-se ao diretório de destino onde todos esses arquivos serão copiados.
|
||||
- *Renomear*: igual ao copiar, mas moverá os arquivos para lá.
|
||||
- *Salvar como*: igual ao copiar, mas gravará lá.
|
||||
- `<M>`: marcar um arquivo para seleção
|
||||
- `<CTRL+A>`: selecionar todos os arquivos no diretório atual
|
||||
- `<ALT+A>`: desselecionar todos os arquivos
|
||||
|
||||
Uma vez marcado, o arquivo será **exibido com fundo destacado** .
|
||||
Ao trabalhar com seleção, apenas os arquivos selecionados serão processados, enquanto o item atualmente destacado será ignorado.
|
||||
|
||||
É possível trabalhar com múltiplos arquivos também no painel de resultados de busca.
|
||||
|
||||
Todas as ações estão disponíveis ao trabalhar com múltiplos arquivos, mas algumas funcionam de forma ligeiramente diferente. Vamos ver:
|
||||
|
||||
- *Copiar*: ao copiar, será solicitado o nome de destino. Com múltiplos arquivos, esse nome será o diretório de destino para todos eles.
|
||||
- *Renomear*: igual a copiar, mas moverá os arquivos.
|
||||
- *Salvar como*: igual a copiar, mas escreverá os arquivos nesse local.
|
||||
|
||||
Se você selecionar um arquivo num diretório (ex: `/home`) e mudar de diretório, ele continuará selecionado e aparecerá na **fila de transferência** no painel inferior.
|
||||
Ao selecionar um arquivo, o diretório *remoto* atual é associado a ele; então, se for transferido, será enviado para esse diretório associado.
|
||||
|
||||
#### Exemplo
|
||||
|
||||
Se selecionarmos `/home/a.txt` localmente e estivermos em `/tmp` no painel remoto, depois mudarmos para `/var` e selecionarmos `/var/b.txt`, e estivermos em `/home` no painel remoto, ao transferir teremos:
|
||||
|
||||
- `/home/a.txt` transferido para `/tmp/a.txt`
|
||||
- `/var/b.txt` transferido para `/home/b.txt`
|
||||
|
||||
### Navegação Sincronizada ⏲️
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
@@ -70,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">由 <a href="https://veeso.dev/" target="_blank">@veeso</a> 开发</p>
|
||||
<p align="center">当前版本: 0.16.1 12/11/2024</p>
|
||||
<p align="center">由 <a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
|
||||
<p align="center">当前版本: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
- [Aws S3 凭证](#aws-s3-凭证)
|
||||
- [文件浏览](#文件浏览)
|
||||
- [快捷键](#快捷键)
|
||||
- [处理多个文件](#处理多个文件)
|
||||
- [操作多个文件 🥷](#操作多个文件-)
|
||||
- [示例](#示例)
|
||||
- [同步浏览](#同步浏览)
|
||||
- [打开/打开方式](#打开打开方式)
|
||||
- [书签](#书签)
|
||||
@@ -254,14 +255,34 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
|
||||
| `<CTRL+C>` | 终止文件传输 | |
|
||||
| `<CTRL+T>` | 显示所有同步路径 | Track |
|
||||
|
||||
### 处理多个文件
|
||||
### 操作多个文件 🥷
|
||||
|
||||
你可以同时操作多个文件,按`<M>`选定它们,或者按`<CTRL+A>` 全选当前工作目录中的所有文件。一旦一个文件被标记为选择,它将在左边显示一个 "*"。在这种模式下,只有选定的文件会被处理,而当前光标高亮显示的项目会被忽略。在查找结果面板中,也可以对多个文件进行处理。
|
||||
在处理多个文件时,所有的操作都是可用的,但请注意,有些操作的工作方式略有不同。让我们深入了解一下:
|
||||
你可以通过以下简单的控制操作多个文件:
|
||||
|
||||
- *复制*: 当你复制一个文件时,你会被提示输入完整目标路径名。当处理多个文件时,这个名称指的是所有这些文件将被复制到的目标目录。
|
||||
- *重命名*: 和复制操作类似, 但是会移动文件到目标路径。
|
||||
- *保存为*: 和复制操作类似, 但是会写入文件到目标路径。
|
||||
- `<M>`:标记文件以进行选择
|
||||
- `<CTRL+A>`:选择当前目录下的所有文件
|
||||
- `<ALT+A>`:取消选择所有文件
|
||||
|
||||
被标记的文件将会以**高亮背景** 显示。
|
||||
当进行选择操作时,只有被选中的文件会执行操作,而当前高亮显示的项目会被忽略。
|
||||
|
||||
即使是在查找结果面板中,也可以操作多个文件。
|
||||
|
||||
在操作多个文件时,所有功能都可用,但某些功能会有些许不同。具体如下:
|
||||
|
||||
- *复制*:复制时会提示你输入目标名称。操作多个文件时,该名称是目标目录,所有文件将被复制到此目录中。
|
||||
- *重命名*:与复制相同,但文件将被移动到该目录。
|
||||
- *另存为*:与复制相同,但文件将被写入该目录。
|
||||
|
||||
如果你在某个目录(如 `/home`)中选择了文件,然后切换目录,文件仍会保持被选中状态,并在底部面板的**传输队列** 中显示。
|
||||
文件被选中时,会将当前*远程*目录与该文件关联;如果文件被传输,它将被传输到与之关联的目录中。
|
||||
|
||||
#### 示例
|
||||
|
||||
如果我们在本地选择 `/home/a.txt`,此时远程目录是 `/tmp`,然后我们切换到 `/var`,选择 `/var/b.txt`,而此时远程目录为 `/home`,执行传输后的结果为:
|
||||
|
||||
- `/home/a.txt` 传输到 `/tmp/a.txt`
|
||||
- `/var/b.txt` 传输到 `/home/b.txt`
|
||||
|
||||
### 同步浏览
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# -f, -y, --force, --yes
|
||||
# Skip the confirmation prompt during installation
|
||||
|
||||
TERMSCP_VERSION="0.16.1"
|
||||
TERMSCP_VERSION="0.18.0"
|
||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
<p class="text-xs font-thin">
|
||||
<span>Christian Visintin © </span><span resolve-copyright></span>
|
||||
<span> | </span>
|
||||
<a class="text-xs font-thin" href="https://veeso.dev/en/privacy" target="_blank">Privacy policy</a>
|
||||
<a class="text-xs font-thin" href="https://veeso.me/en/privacy" target="_blank">Privacy policy</a>
|
||||
<span> | </span>
|
||||
<a class="text-xs font-thin" href="https://veeso.dev/en/cookie-policy" target="_blank">Cookie policy</a>
|
||||
<a class="text-xs font-thin" href="https://veeso.me/en/cookie-policy" target="_blank">Cookie policy</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
||||
since last release, so if the latest version is not available yet,
|
||||
you can install it downloading the ZIP file from</span>
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.16.1.nupkg"
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.18.0.nupkg"
|
||||
target="_blank">Github</a>
|
||||
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
||||
</p>
|
||||
@@ -74,7 +74,7 @@
|
||||
On Debian based distros, you can install termscp using the Deb
|
||||
package via:
|
||||
</p>
|
||||
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.16.1_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.18.0_amd64.deb</span>
|
||||
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
|
||||
</div>
|
||||
<h3>
|
||||
@@ -157,7 +157,7 @@ sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</spa
|
||||
</p>
|
||||
<pre><span class="function">cargo</span> install --locked --no-default-features --features smb <span class="string">termscp</span></pre>
|
||||
<p translate="getStarted.cargo.noSMB" class="pt-4 pb-2"></p>
|
||||
<pre><span class="function">cargo</span> install --locked --no-default-features --features with-keyring <span class="string">termscp</span></pre>
|
||||
<pre><span class="function">cargo</span> install --locked --no-default-features --features keyring <span class="string">termscp</span></pre>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</button>
|
||||
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
||||
<p class="text-lg">
|
||||
<span translate="intro.versionAlert">termscp 0.16.1 is NOW out! Download it from</span>
|
||||
<span translate="intro.versionAlert">termscp 0.18.0 is NOW out! Download it from</span>
|
||||
<a href="/get-started.html" translate="intro.here">here!</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Get started →",
|
||||
"versionAlert": "termscp 0.16.1 is NOW out! Download it from",
|
||||
"versionAlert": "termscp 0.18.0 is NOW out! Download it from",
|
||||
"here": "here",
|
||||
"features": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Para iniciar →",
|
||||
"versionAlert": "termscp 0.16.1 ya está disponible! Descárgalo desde",
|
||||
"versionAlert": "termscp 0.18.0 ya está disponible! Descárgalo desde",
|
||||
"here": "aquì",
|
||||
"features": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Pour commencer →",
|
||||
"versionAlert": "termscp 0.16.1 est maintenant sorti! Télécharge-le depuis",
|
||||
"versionAlert": "termscp 0.18.0 est maintenant sorti! Télécharge-le depuis",
|
||||
"here": "ici",
|
||||
"features": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
||||
"getStarted": "Installa termscp →",
|
||||
"versionAlert": "termscp 0.16.1 è ORA disponbile! Scaricalo da",
|
||||
"versionAlert": "termscp 0.18.0 è ORA disponbile! Scaricalo da",
|
||||
"here": "qui",
|
||||
"features": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "开始 →",
|
||||
"versionAlert": "termscp 0.16.1 现已发布! 从下载",
|
||||
"versionAlert": "termscp 0.18.0 现已发布! 从下载",
|
||||
"here": "这里",
|
||||
"features": {
|
||||
"handy": {
|
||||
@@ -112,4 +112,4 @@
|
||||
"then": "启动后,系统将提示您是否安装更新。 确认安装和 ta-dah,新版本的termscp 现在应该可以在你的机器上使用了"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -50,7 +50,7 @@ pub struct ActivityManager {
|
||||
|
||||
impl ActivityManager {
|
||||
/// Initializes a new Activity Manager
|
||||
pub fn new(ticks: Duration) -> Result<ActivityManager, HostError> {
|
||||
pub fn new(ticks: Duration, keyring: bool) -> Result<ActivityManager, HostError> {
|
||||
// Prepare Context
|
||||
// Initialize configuration client
|
||||
let (config_client, error_config): (ConfigClient, Option<String>) =
|
||||
@@ -61,7 +61,7 @@ impl ActivityManager {
|
||||
(ConfigClient::degraded(), Some(err))
|
||||
}
|
||||
};
|
||||
let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client() {
|
||||
let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client(keyring) {
|
||||
Ok(cli) => (cli, None),
|
||||
Err(err) => (None, Some(err)),
|
||||
};
|
||||
@@ -90,13 +90,18 @@ impl ActivityManager {
|
||||
)),
|
||||
host_params.password.as_deref(),
|
||||
),
|
||||
Remote::None => self.set_host_params(
|
||||
HostParams::HostBridge(HostBridgeParams::Localhost(
|
||||
env::current_dir()
|
||||
.map_err(|e| format!("Could not get current directory: {e}"))?,
|
||||
)),
|
||||
None,
|
||||
),
|
||||
Remote::None => {
|
||||
// local dir is remote_args.local_dir if set, otherwise current dir
|
||||
let local_dir = remote_args
|
||||
.local_dir
|
||||
.unwrap_or_else(|| env::current_dir().unwrap());
|
||||
debug!("host bridge is None, setting local dir to {:?}", local_dir,);
|
||||
|
||||
self.set_host_params(
|
||||
HostParams::HostBridge(HostBridgeParams::Localhost(local_dir)),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}?;
|
||||
|
||||
// set remote
|
||||
@@ -243,7 +248,7 @@ impl ActivityManager {
|
||||
None => {
|
||||
return Err(format!(
|
||||
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
|
||||
))
|
||||
));
|
||||
}
|
||||
Some(params) => params,
|
||||
};
|
||||
@@ -342,7 +347,7 @@ impl ActivityManager {
|
||||
fn run_filetransfer(&mut self) -> Option<NextActivity> {
|
||||
info!("Starting FileTransferActivity");
|
||||
// Get context
|
||||
let ctx: Context = match self.context.take() {
|
||||
let mut ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
None => {
|
||||
error!("Failed to start FileTransferActivity: context is None");
|
||||
@@ -367,8 +372,18 @@ impl ActivityManager {
|
||||
}
|
||||
};
|
||||
|
||||
let mut activity: FileTransferActivity =
|
||||
FileTransferActivity::new(host_bridge_params, remote_params, self.ticks);
|
||||
// try to setup activity
|
||||
let mut activity =
|
||||
match FileTransferActivity::new(host_bridge_params, remote_params, self.ticks) {
|
||||
Ok(activity) => activity,
|
||||
Err(err) => {
|
||||
error!("Failed to start FileTransferActivity: {}", err);
|
||||
ctx.set_error(err);
|
||||
self.context = Some(ctx);
|
||||
// Return to authentication
|
||||
return Some(NextActivity::Authentication);
|
||||
}
|
||||
};
|
||||
// Prepare result
|
||||
let result: Option<NextActivity>;
|
||||
// Create activity
|
||||
@@ -432,7 +447,7 @@ impl ActivityManager {
|
||||
|
||||
// -- misc
|
||||
|
||||
fn init_bookmarks_client() -> Result<Option<BookmarksClient>, String> {
|
||||
fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
|
||||
// Get config dir
|
||||
match environment::init_config_dir() {
|
||||
Ok(path) => {
|
||||
@@ -441,16 +456,21 @@ impl ActivityManager {
|
||||
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)
|
||||
.map(Option::Some)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
|
||||
bookmarks_file.display(),
|
||||
config_dir_path.display(),
|
||||
e
|
||||
)
|
||||
})
|
||||
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)
|
||||
}
|
||||
@@ -495,19 +515,28 @@ impl ActivityManager {
|
||||
match ThemeProvider::new(theme_path.as_path()) {
|
||||
Ok(provider) => provider,
|
||||
Err(err) => {
|
||||
error!("Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode", theme_path.display(), err);
|
||||
error!(
|
||||
"Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode",
|
||||
theme_path.display(),
|
||||
err
|
||||
);
|
||||
ThemeProvider::degraded()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("This system doesn't provide a configuration directory; using theme provider in degraded mode");
|
||||
error!(
|
||||
"This system doesn't provide a configuration directory; using theme provider in degraded mode"
|
||||
);
|
||||
ThemeProvider::degraded()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Could not initialize configuration directory: {}; using theme provider in degraded mode", err);
|
||||
error!(
|
||||
"Could not initialize configuration directory: {}; using theme provider in degraded mode",
|
||||
err
|
||||
);
|
||||
ThemeProvider::degraded()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ pub enum Task {
|
||||
Activity(NextActivity),
|
||||
ImportTheme(PathBuf),
|
||||
InstallUpdate,
|
||||
Version,
|
||||
}
|
||||
|
||||
#[derive(Default, FromArgs)]
|
||||
@@ -59,6 +60,9 @@ pub struct Args {
|
||||
/// print version
|
||||
#[argh(switch, short = 'v')]
|
||||
pub version: bool,
|
||||
/// disable keyring support
|
||||
#[argh(switch)]
|
||||
pub wno_keyring: bool,
|
||||
// -- positional
|
||||
#[argh(positional, description = "address1 address2 local-wrkdir")]
|
||||
pub positional: Vec<String>,
|
||||
@@ -93,6 +97,7 @@ pub struct LoadThemeArgs {
|
||||
|
||||
pub struct RunOpts {
|
||||
pub remote: RemoteArgs,
|
||||
pub keyring: bool,
|
||||
pub ticks: Duration,
|
||||
pub log_level: LogLevel,
|
||||
pub task: Task,
|
||||
@@ -126,6 +131,7 @@ impl Default for RunOpts {
|
||||
Self {
|
||||
remote: RemoteArgs::default(),
|
||||
ticks: Duration::from_millis(10),
|
||||
keyring: true,
|
||||
log_level: LogLevel::Info,
|
||||
task: Task::Activity(NextActivity::Authentication),
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Contains the error for serializer/deserializer
|
||||
@@ -63,7 +63,7 @@ where
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::Serialization,
|
||||
err.to_string(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
trace!("Serialized new bookmarks data: {}", data);
|
||||
|
||||
@@ -364,8 +364,16 @@ impl Formatter {
|
||||
if fsentry.is_file() {
|
||||
// Get byte size
|
||||
let size: ByteSize = ByteSize(fsentry.metadata().size);
|
||||
let mut fmt = size.display().si().to_string();
|
||||
// pad with up to len 10
|
||||
let pad = 10usize.saturating_sub(fmt.len());
|
||||
for _ in 0..pad {
|
||||
fmt.push(' ');
|
||||
}
|
||||
|
||||
format!("{cur_str}{prefix}{fmt}")
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{cur_str}{prefix}{size:10}")
|
||||
//format!("{cur_str}{prefix}{size:10}", size = size.display().si())
|
||||
} else if fsentry.metadata().symlink.is_some() {
|
||||
let size = ByteSize(
|
||||
fsentry
|
||||
@@ -376,7 +384,14 @@ impl Formatter {
|
||||
.to_string_lossy()
|
||||
.len() as u64,
|
||||
);
|
||||
format!("{cur_str}{prefix}{size:10}")
|
||||
let mut fmt = size.display().si().to_string();
|
||||
// pad with up to len 10
|
||||
let pad = 10usize.saturating_sub(fmt.len());
|
||||
for _ in 0..pad {
|
||||
fmt.push(' ');
|
||||
}
|
||||
|
||||
format!("{cur_str}{prefix}{fmt}")
|
||||
} else {
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{cur_str}{prefix} ")
|
||||
@@ -489,10 +504,7 @@ impl Formatter {
|
||||
};
|
||||
// Match format length: group 3
|
||||
let fmt_len: Option<usize> = match ®ex_match.get(3) {
|
||||
Some(len) => match len.as_str().parse::<usize>() {
|
||||
Ok(len) => Some(len),
|
||||
Err(_) => None,
|
||||
},
|
||||
Some(len) => len.as_str().parse::<usize>().ok(),
|
||||
None => None,
|
||||
};
|
||||
// Match format extra: group 2 + 1
|
||||
@@ -596,7 +608,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- root 8.2 KB {}",
|
||||
"bar.txt -rw-r--r-- root 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -604,7 +616,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- 0 8.2 KB {}",
|
||||
"bar.txt -rw-r--r-- 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -627,7 +639,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperupup… -rw-r--r-- root 8.2 KB {}",
|
||||
"piroparoporoperoperupup… -rw-r--r-- root 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -635,7 +647,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 KB {}",
|
||||
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -658,7 +670,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? root 8.2 KB {}",
|
||||
"bar.txt -????????? root 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -666,7 +678,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
"bar.txt -????????? 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -689,7 +701,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
"bar.txt -????????? 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -697,7 +709,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
"bar.txt -????????? 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -774,8 +786,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_all_together_now() {
|
||||
let formatter: Formatter =
|
||||
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
|
||||
let formatter: Formatter = Formatter::new(
|
||||
"{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}",
|
||||
);
|
||||
// Directory (with symlink)
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
@@ -792,12 +805,15 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
};
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"projects -> project.info 0 0 lrwxr-xr-x 12 B {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects -> project.info 0 0 lrwxr-xr-x 12 B {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
)
|
||||
);
|
||||
// Directory without symlink
|
||||
let entry = File {
|
||||
path: PathBuf::from("/home/cvisintin/projects"),
|
||||
@@ -813,12 +829,15 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
};
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"projects/ 0 0 drwxr-xr-x {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ 0 0 drwxr-xr-x {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
)
|
||||
);
|
||||
// File with symlink
|
||||
let entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
@@ -834,12 +853,15 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"bar.txt -> project.info 0 0 lrw-r--r-- 12 B {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -> project.info 0 0 lrw-r--r-- 12 B {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
)
|
||||
);
|
||||
// File without symlink
|
||||
let entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
@@ -855,12 +877,15 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt 0 0 -rw-r--r-- 8.2 kB {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,7 +7,7 @@ pub(crate) mod builder;
|
||||
mod formatter;
|
||||
// Locals
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -42,14 +42,26 @@ pub enum GroupDirs {
|
||||
|
||||
/// File explorer states
|
||||
pub struct FileExplorer {
|
||||
pub wrkdir: PathBuf, // Current directory
|
||||
pub(crate) dirstack: VecDeque<PathBuf>, // Stack of visited directory (max 16)
|
||||
pub(crate) stack_size: usize, // Directory stack size
|
||||
pub(crate) file_sorting: FileSorting, // File sorting criteria
|
||||
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
|
||||
pub(crate) opts: ExplorerOpts, // Explorer options
|
||||
pub(crate) fmt: Formatter, // File formatter
|
||||
files: Vec<File>, // Files in directory
|
||||
/// Current working directory
|
||||
pub wrkdir: PathBuf,
|
||||
/// Stack of visited directories
|
||||
pub(crate) dirstack: VecDeque<PathBuf>,
|
||||
/// Stack size
|
||||
pub(crate) stack_size: usize,
|
||||
/// Criteria to sort file
|
||||
pub(crate) file_sorting: FileSorting,
|
||||
/// defines how to group directories in the explorer
|
||||
pub(crate) group_dirs: Option<GroupDirs>,
|
||||
/// Explorer options
|
||||
pub(crate) opts: ExplorerOpts,
|
||||
/// Formatter for file entries
|
||||
pub(crate) fmt: Formatter,
|
||||
/// Is terminal open for this explorer?
|
||||
terminal: bool,
|
||||
/// Files in directory
|
||||
files: Vec<File>,
|
||||
/// files enqueued for transfer. Map between source and destination
|
||||
transfer_queue: HashMap<PathBuf, PathBuf>, // transfer queue
|
||||
}
|
||||
|
||||
impl Default for FileExplorer {
|
||||
@@ -63,6 +75,8 @@ impl Default for FileExplorer {
|
||||
opts: ExplorerOpts::empty(),
|
||||
fmt: Formatter::default(),
|
||||
files: Vec::new(),
|
||||
terminal: false,
|
||||
transfer_queue: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +153,44 @@ impl FileExplorer {
|
||||
filtered.get(idx).copied()
|
||||
}
|
||||
|
||||
/// Enqueue a file for transfer
|
||||
pub fn enqueue(&mut self, src: &Path, dst: &Path) {
|
||||
self.transfer_queue
|
||||
.insert(PathBuf::from(src), PathBuf::from(dst));
|
||||
}
|
||||
|
||||
/// Enqueue all files for transfer
|
||||
pub fn enqueue_all(&mut self, dst: &Path) {
|
||||
let files: Vec<_> = self.iter_files().map(|f| f.path.clone()).collect();
|
||||
for file in files {
|
||||
self.enqueue(&file, dst);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get enqueued files
|
||||
pub fn enqueued(&self) -> &HashMap<PathBuf, PathBuf> {
|
||||
&self.transfer_queue
|
||||
}
|
||||
|
||||
/// Dequeue a file
|
||||
pub fn dequeue(&mut self, src: &Path) {
|
||||
self.transfer_queue.remove(src);
|
||||
}
|
||||
|
||||
/// Clear transfer queue
|
||||
pub fn clear_queue(&mut self) {
|
||||
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
|
||||
|
||||
/// Format a file entry
|
||||
@@ -523,7 +575,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
explorer.fmt_file(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- root 8.2 KB {}",
|
||||
"bar.txt -rw-r--r-- root 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -531,7 +583,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
explorer.fmt_file(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- 0 8.2 KB {}",
|
||||
"bar.txt -rw-r--r-- 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -586,6 +638,26 @@ mod tests {
|
||||
assert_eq!(explorer.files.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_enqueue_and_dequeue_files() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
// Create files (files are then sorted by name)
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry("CONTRIBUTING.md", false),
|
||||
make_fs_entry("docs", true),
|
||||
make_fs_entry("src", true),
|
||||
make_fs_entry("README.md", false),
|
||||
]);
|
||||
// Enqueue
|
||||
explorer.enqueue(Path::new("CONTRIBUTING.md"), Path::new("CONTRIBUTING.md"));
|
||||
explorer.enqueue(Path::new("docs"), Path::new("docs"));
|
||||
// Dequeue
|
||||
explorer.dequeue(Path::new("CONTRIBUTING.md"));
|
||||
assert_eq!(explorer.enqueued().len(), 1);
|
||||
explorer.dequeue(Path::new("docs"));
|
||||
assert_eq!(explorer.enqueued().len(), 0);
|
||||
}
|
||||
|
||||
fn make_fs_entry(name: &str, is_dir: bool) -> File {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let metadata = Metadata {
|
||||
|
||||
@@ -7,15 +7,19 @@ pub struct HostBridgeBuilder;
|
||||
impl HostBridgeBuilder {
|
||||
/// Build Host Bridge from parms
|
||||
///
|
||||
/// if protocol and parameters are inconsistent, the function will panic.
|
||||
pub fn build(params: HostBridgeParams, config_client: &ConfigClient) -> Box<dyn HostBridge> {
|
||||
/// if protocol and parameters are inconsistent, the function will return an error.
|
||||
pub fn build(
|
||||
params: HostBridgeParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Result<Box<dyn HostBridge>, String> {
|
||||
match params {
|
||||
HostBridgeParams::Localhost(path) => {
|
||||
Box::new(Localhost::new(path).expect("Failed to create Localhost"))
|
||||
HostBridgeParams::Localhost(path) => Localhost::new(path)
|
||||
.map(|host| Box::new(host) as Box<dyn HostBridge>)
|
||||
.map_err(|e| e.to_string()),
|
||||
HostBridgeParams::Remote(protocol, params) => {
|
||||
RemoteFsBuilder::build(protocol, params, config_client)
|
||||
.map(|host| Box::new(RemoteBridged::from(host)) as Box<dyn HostBridge>)
|
||||
}
|
||||
HostBridgeParams::Remote(protocol, params) => Box::new(RemoteBridged::from(
|
||||
RemoteFsBuilder::build(protocol, params, config_client),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,16 @@ impl HostBridgeParams {
|
||||
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
|
||||
@@ -42,6 +52,15 @@ pub struct FileTransferParams {
|
||||
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
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProtocolParams {
|
||||
@@ -301,11 +320,13 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn password_missing() {
|
||||
assert!(FileTransferParams::new(
|
||||
FileTransferProtocol::Scp,
|
||||
ProtocolParams::AwsS3(AwsS3Params::new("omar", Some("eu-west-1"), Some("test")))
|
||||
)
|
||||
.password_missing());
|
||||
assert!(
|
||||
FileTransferParams::new(
|
||||
FileTransferProtocol::Scp,
|
||||
ProtocolParams::AwsS3(AwsS3Params::new("omar", Some("eu-west-1"), Some("test")))
|
||||
)
|
||||
.password_missing()
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferParams::new(
|
||||
FileTransferProtocol::Scp,
|
||||
|
||||
@@ -37,40 +37,50 @@ impl RemoteFsBuilder {
|
||||
protocol: FileTransferProtocol,
|
||||
params: ProtocolParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Box<dyn RemoteFs> {
|
||||
) -> Result<Box<dyn RemoteFs>, String> {
|
||||
match (protocol, params) {
|
||||
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => {
|
||||
Box::new(Self::aws_s3_client(params))
|
||||
Ok(Box::new(Self::aws_s3_client(params)))
|
||||
}
|
||||
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
|
||||
Box::new(Self::ftp_client(params, secure))
|
||||
Ok(Box::new(Self::ftp_client(params, secure)))
|
||||
}
|
||||
(FileTransferProtocol::Kube, ProtocolParams::Kube(params)) => {
|
||||
Box::new(Self::kube_client(params))
|
||||
Ok(Box::new(Self::kube_client(params)))
|
||||
}
|
||||
(FileTransferProtocol::Scp, ProtocolParams::Generic(params)) => {
|
||||
Box::new(Self::scp_client(params, config_client))
|
||||
Ok(Box::new(Self::scp_client(params, config_client)))
|
||||
}
|
||||
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
|
||||
Box::new(Self::sftp_client(params, config_client))
|
||||
Ok(Box::new(Self::sftp_client(params, config_client)))
|
||||
}
|
||||
#[cfg(smb)]
|
||||
(FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => {
|
||||
Box::new(Self::smb_client(params))
|
||||
Ok(Box::new(Self::smb_client(params)))
|
||||
}
|
||||
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
|
||||
Box::new(Self::webdav_client(params))
|
||||
Ok(Box::new(Self::webdav_client(params)))
|
||||
}
|
||||
(protocol, params) => {
|
||||
error!("Invalid params for protocol '{:?}'", protocol);
|
||||
panic!("Invalid protocol '{protocol:?}' with parameters of type {params:?}")
|
||||
Err(format!(
|
||||
"Invalid protocol '{protocol:?}' with parameters of type {params:?}",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build aws s3 client from parameters
|
||||
fn aws_s3_client(params: AwsS3Params) -> AwsS3Fs {
|
||||
let mut client = AwsS3Fs::new(params.bucket_name).new_path_style(params.new_path_style);
|
||||
let rt = Arc::new(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Unable to create tokio runtime"),
|
||||
);
|
||||
let mut client =
|
||||
AwsS3Fs::new(params.bucket_name, &rt).new_path_style(params.new_path_style);
|
||||
if let Some(region) = params.region {
|
||||
client = client.region(region);
|
||||
}
|
||||
@@ -223,8 +233,12 @@ impl RemoteFsBuilder {
|
||||
debug!("no username was provided, using current username");
|
||||
opts = opts.username(whoami::username());
|
||||
}
|
||||
// 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 {
|
||||
opts = opts.password(password);
|
||||
if !password.is_empty() {
|
||||
opts = opts.password(password);
|
||||
}
|
||||
}
|
||||
if let Some(config_path) = config_client.get_ssh_config() {
|
||||
opts = opts.config_file(
|
||||
@@ -262,7 +276,9 @@ mod test {
|
||||
.session_token(Some("gerry-scotti")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||
assert!(
|
||||
RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client).is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -275,7 +291,9 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::Ftp(true), params, &config_client);
|
||||
assert!(
|
||||
RemoteFsBuilder::build(FileTransferProtocol::Ftp(true), params, &config_client).is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -288,7 +306,7 @@ mod test {
|
||||
client_key: Some("client_key".to_string()),
|
||||
});
|
||||
let config_client = get_config_client();
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -301,7 +319,7 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::Scp, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Scp, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -314,7 +332,7 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::Sftp, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Sftp, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -322,11 +340,10 @@ mod test {
|
||||
fn should_build_smb_fs() {
|
||||
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
|
||||
let config_client = get_config_client();
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn should_not_build_fs() {
|
||||
let params = ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
@@ -336,7 +353,9 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||
assert!(
|
||||
RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
fn get_config_client() -> ConfigClient {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use remotefs::fs::{Metadata, UnixPex};
|
||||
use remotefs::File;
|
||||
use remotefs::fs::{Metadata, UnixPex};
|
||||
|
||||
use super::HostResult;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ use std::os::unix::fs::PermissionsExt as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use filetime::FileTime;
|
||||
use remotefs::fs::{FileType, Metadata, UnixPex};
|
||||
use remotefs::File;
|
||||
use remotefs::fs::{FileType, Metadata, UnixPex};
|
||||
|
||||
use super::{HostBridge, HostResult};
|
||||
use crate::host::{HostError, HostErrorType};
|
||||
@@ -105,8 +105,8 @@ impl HostBridge for Localhost {
|
||||
));
|
||||
}
|
||||
let prev_dir: PathBuf = self.wrkdir.clone(); // Backup location
|
||||
// Update working directory
|
||||
// Change dir
|
||||
// Update working directory
|
||||
// Change dir
|
||||
self.wrkdir = new_dir;
|
||||
// Scan new directory
|
||||
let pwd = self.pwd()?;
|
||||
@@ -135,7 +135,7 @@ impl HostBridge for Localhost {
|
||||
HostErrorType::FileAlreadyExists,
|
||||
None,
|
||||
dir_path.as_path(),
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,7 +559,7 @@ mod tests {
|
||||
use std::io::Write;
|
||||
use std::ops::AddAssign;
|
||||
#[cfg(posix)]
|
||||
use std::os::unix::fs::{symlink, PermissionsExt};
|
||||
use std::os::unix::fs::{PermissionsExt, symlink};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -579,6 +579,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
#[cfg(not(feature = "isolated-tests"))]
|
||||
fn test_host_localhost_new() {
|
||||
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
assert_eq!(host.wrkdir, PathBuf::from("/dev"));
|
||||
@@ -622,6 +623,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
#[cfg(not(feature = "isolated-tests"))]
|
||||
fn test_host_localhost_change_dir() {
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
let new_dir: PathBuf = PathBuf::from("/dev");
|
||||
@@ -659,9 +661,10 @@ mod tests {
|
||||
#[should_panic]
|
||||
fn test_host_localhost_open_read_err_no_such_file() {
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
assert!(host
|
||||
.open_file(PathBuf::from("/bin/foo-bar-test-omar-123-456-789.txt").as_path())
|
||||
.is_ok());
|
||||
assert!(
|
||||
host.open_file(PathBuf::from("/bin/foo-bar-test-omar-123-456-789.txt").as_path())
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -702,11 +705,13 @@ mod tests {
|
||||
// Create sample file
|
||||
assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
|
||||
// Create symlink
|
||||
assert!(symlink(
|
||||
format!("{}/foo.txt", tmpdir.path().display()),
|
||||
format!("{}/bar.txt", tmpdir.path().display())
|
||||
)
|
||||
.is_ok());
|
||||
assert!(
|
||||
symlink(
|
||||
format!("{}/foo.txt", tmpdir.path().display()),
|
||||
format!("{}/bar.txt", tmpdir.path().display())
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
// Get dir
|
||||
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
@@ -742,19 +747,21 @@ mod tests {
|
||||
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||
// Try to re-create directory
|
||||
// Try to re-create directory
|
||||
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
|
||||
// Try abs path
|
||||
assert!(host
|
||||
.mkdir_ex(PathBuf::from("/tmp/test_dir_123456789").as_path(), true)
|
||||
.is_ok());
|
||||
assert!(
|
||||
host.mkdir_ex(PathBuf::from("/tmp/test_dir_123456789").as_path(), true)
|
||||
.is_ok()
|
||||
);
|
||||
// Fail
|
||||
assert!(host
|
||||
.mkdir_ex(
|
||||
assert!(
|
||||
host.mkdir_ex(
|
||||
PathBuf::from("/aaaa/oooooo/tmp/test_dir_123456789").as_path(),
|
||||
true
|
||||
)
|
||||
.is_err());
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -766,24 +773,26 @@ mod tests {
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||
// Remove file
|
||||
// Remove file
|
||||
assert!(host.remove(files.get(0).unwrap()).is_ok());
|
||||
// There should be 0 files now
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 0); // There should be 0 files now
|
||||
// Create directory
|
||||
// Create directory
|
||||
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
|
||||
// Delete directory
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||
assert!(host.remove(files.get(0).unwrap()).is_ok());
|
||||
// Remove unexisting directory
|
||||
assert!(host
|
||||
.remove(&make_fsentry(PathBuf::from("/a/b/c/d"), true))
|
||||
.is_err());
|
||||
assert!(host
|
||||
.remove(&make_fsentry(PathBuf::from("/aaaaaaa"), false))
|
||||
.is_err());
|
||||
assert!(
|
||||
host.remove(&make_fsentry(PathBuf::from("/a/b/c/d"), true))
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
host.remove(&make_fsentry(PathBuf::from("/aaaaaaa"), false))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -801,18 +810,20 @@ mod tests {
|
||||
// Rename file
|
||||
let dst_path: PathBuf =
|
||||
PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str());
|
||||
assert!(host
|
||||
.rename(files.get(0).unwrap(), dst_path.as_path())
|
||||
.is_ok());
|
||||
assert!(
|
||||
host.rename(files.get(0).unwrap(), dst_path.as_path())
|
||||
.is_ok()
|
||||
);
|
||||
// There should be still 1 file now, but named bar.txt
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 1); // There should be 0 files now
|
||||
assert_eq!(files.get(0).unwrap().name(), "bar.txt");
|
||||
// Fail
|
||||
let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu");
|
||||
assert!(host
|
||||
.rename(files.get(0).unwrap(), bad_path.as_path())
|
||||
.is_err());
|
||||
assert!(
|
||||
host.rename(files.get(0).unwrap(), bad_path.as_path())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -851,12 +862,13 @@ mod tests {
|
||||
// Chmod to dir
|
||||
assert!(host.chmod(tmpdir.path(), UnixPex::from(0o750)).is_ok());
|
||||
// Error
|
||||
assert!(host
|
||||
.chmod(
|
||||
assert!(
|
||||
host.chmod(
|
||||
Path::new("/tmp/krgiogoiegj/kwrgnoerig"),
|
||||
UnixPex::from(0o777)
|
||||
)
|
||||
.is_err());
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
@@ -881,12 +893,13 @@ mod tests {
|
||||
// Verify host has two files
|
||||
assert_eq!(host.files.len(), 2);
|
||||
// Fail copy
|
||||
assert!(host
|
||||
.copy(
|
||||
assert!(
|
||||
host.copy(
|
||||
&make_fsentry(PathBuf::from("/a/a7/a/a7a"), false),
|
||||
PathBuf::from("571k422i").as_path()
|
||||
)
|
||||
.is_err());
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
@@ -994,9 +1007,10 @@ mod tests {
|
||||
assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_ok());
|
||||
// Fail symlink
|
||||
assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_err());
|
||||
assert!(host
|
||||
.symlink(Path::new("/tmp/oooo/aaaa"), p.as_path())
|
||||
.is_err());
|
||||
assert!(
|
||||
host.symlink(Path::new("/tmp/oooo/aaaa"), p.as_path())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -44,7 +44,7 @@ pub enum HostErrorType {
|
||||
}
|
||||
|
||||
/// HostError is a wrapper for the error type and the exact io error
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub struct HostError {
|
||||
pub error: HostErrorType,
|
||||
ioerr: Option<std::io::Error>,
|
||||
|
||||
67
src/main.rs
67
src/main.rs
@@ -33,24 +33,24 @@ const APP_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
const APP_BUILD_DATE: &str = env!("VERGEN_BUILD_TIMESTAMP");
|
||||
const APP_GIT_BRANCH: &str = env!("VERGEN_GIT_BRANCH");
|
||||
const APP_GIT_HASH: &str = env!("VERGEN_GIT_SHA");
|
||||
const EXIT_CODE_SUCCESS: i32 = 0;
|
||||
const EXIT_CODE_ERROR: i32 = 1;
|
||||
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
|
||||
type MainResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
#[inline]
|
||||
fn git_hash() -> &'static str {
|
||||
APP_GIT_HASH[0..8].as_ref()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> MainResult<()> {
|
||||
let args: Args = argh::from_env();
|
||||
// Parse args
|
||||
let run_opts: RunOpts = match parse_args(args) {
|
||||
Ok(opts) => opts,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
std::process::exit(255);
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
// Setup logging
|
||||
@@ -63,10 +63,7 @@ fn main() {
|
||||
);
|
||||
// Run
|
||||
info!("Starting activity manager...");
|
||||
let rc = run(run_opts);
|
||||
info!("termscp terminated with exitcode {}", rc);
|
||||
// Then return
|
||||
std::process::exit(rc);
|
||||
run(run_opts)
|
||||
}
|
||||
|
||||
/// Parse arguments
|
||||
@@ -79,12 +76,11 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
Some(ArgsSubcommands::Config(_)) => RunOpts::config(),
|
||||
None => {
|
||||
let mut run_opts: RunOpts = RunOpts::default();
|
||||
|
||||
// Version
|
||||
if args.version {
|
||||
return Err(format!(
|
||||
"{APP_NAME} v{TERMSCP_VERSION} ({APP_GIT_BRANCH}, {git_hash}, {APP_BUILD_DATE}) - Developed by {TERMSCP_AUTHORS}",
|
||||
git_hash = git_hash()
|
||||
));
|
||||
run_opts.task = Task::Version;
|
||||
return Ok(run_opts);
|
||||
}
|
||||
// Logging
|
||||
if args.debug {
|
||||
@@ -92,6 +88,10 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
} else if args.quiet {
|
||||
run_opts.log_level = LogLevel::Off;
|
||||
}
|
||||
// set keyring
|
||||
if args.wno_keyring {
|
||||
run_opts.keyring = false;
|
||||
}
|
||||
// Match ticks
|
||||
run_opts.ticks = Duration::from_millis(args.ticks);
|
||||
// Remote argument
|
||||
@@ -125,57 +125,74 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
}
|
||||
|
||||
/// Run task and return rc
|
||||
fn run(run_opts: RunOpts) -> i32 {
|
||||
fn run(run_opts: RunOpts) -> MainResult<()> {
|
||||
match run_opts.task {
|
||||
Task::ImportTheme(theme) => run_import_theme(&theme),
|
||||
Task::InstallUpdate => run_install_update(),
|
||||
Task::Activity(activity) => run_activity(activity, run_opts.ticks, run_opts.remote),
|
||||
Task::Activity(activity) => {
|
||||
run_activity(activity, run_opts.ticks, run_opts.remote, run_opts.keyring)
|
||||
}
|
||||
Task::Version => print_version(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_import_theme(theme: &Path) -> i32 {
|
||||
fn print_version() -> MainResult<()> {
|
||||
println!(
|
||||
"{APP_NAME} v{TERMSCP_VERSION} ({APP_GIT_BRANCH}, {git_hash}, {APP_BUILD_DATE}) - Developed by {TERMSCP_AUTHORS}",
|
||||
git_hash = git_hash()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_import_theme(theme: &Path) -> MainResult<()> {
|
||||
match support::import_theme(theme) {
|
||||
Ok(_) => {
|
||||
println!("Theme has been successfully imported!");
|
||||
EXIT_CODE_ERROR
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
EXIT_CODE_ERROR
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_install_update() -> i32 {
|
||||
fn run_install_update() -> MainResult<()> {
|
||||
match support::install_update() {
|
||||
Ok(msg) => {
|
||||
println!("{msg}");
|
||||
EXIT_CODE_SUCCESS
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Could not install update: {err}");
|
||||
EXIT_CODE_ERROR
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_activity(activity: NextActivity, ticks: Duration, remote_args: RemoteArgs) -> i32 {
|
||||
fn run_activity(
|
||||
activity: NextActivity,
|
||||
ticks: Duration,
|
||||
remote_args: RemoteArgs,
|
||||
keyring: bool,
|
||||
) -> MainResult<()> {
|
||||
// Create activity manager (and context too)
|
||||
let mut manager: ActivityManager = match ActivityManager::new(ticks) {
|
||||
let mut manager: ActivityManager = match ActivityManager::new(ticks, keyring) {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
eprintln!("Could not start activity manager: {err}");
|
||||
return EXIT_CODE_ERROR;
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
// Set file transfer params if set
|
||||
if let Err(err) = manager.configure_remote_args(remote_args) {
|
||||
eprintln!("{err}");
|
||||
return EXIT_CODE_ERROR;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
manager.run(activity);
|
||||
|
||||
EXIT_CODE_SUCCESS
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -79,10 +79,7 @@ fn get_config_client() -> Option<ConfigClient> {
|
||||
Err(_) => None,
|
||||
Ok(dir) => {
|
||||
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()) {
|
||||
Err(_) => None,
|
||||
Ok(c) => Some(c),
|
||||
}
|
||||
ConfigClient::new(cfg_path.as_path(), ssh_key_dir.as_path()).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use self_update::backends::github::Update as GithubUpdater;
|
||||
pub use self_update::errors::Error as UpdateError;
|
||||
use self_update::update::Release as UpdRelease;
|
||||
use self_update::{cargo_crate_version, Status};
|
||||
use self_update::{Status, cargo_crate_version};
|
||||
|
||||
use crate::utils::parser::parse_semver;
|
||||
|
||||
|
||||
@@ -10,13 +10,12 @@ use std::string::ToString;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use super::keys::filestorage::FileStorage;
|
||||
#[cfg(feature = "with-keyring")]
|
||||
use super::keys::keyringstorage::KeyringStorage;
|
||||
use super::keys::{KeyStorage, KeyStorageError};
|
||||
// Local
|
||||
use crate::config::{
|
||||
bookmarks::{Bookmark, UserHosts},
|
||||
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
||||
serialization::{SerializerError, SerializerErrorKind, deserialize, serialize},
|
||||
};
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::utils::crypto;
|
||||
@@ -39,42 +38,13 @@ impl BookmarksClient {
|
||||
bookmarks_file: &Path,
|
||||
storage_path: &Path,
|
||||
recents_size: usize,
|
||||
keyring: bool,
|
||||
) -> Result<BookmarksClient, SerializerError> {
|
||||
// Create default hosts
|
||||
let default_hosts: UserHosts = UserHosts::default();
|
||||
debug!("Setting up bookmarks client...");
|
||||
// Make a key storage (with-keyring)
|
||||
#[cfg(feature = "with-keyring")]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
debug!("Setting up KeyStorage");
|
||||
let username: String = whoami::username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
// Check if keyring storage is supported
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "termscp";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "termscp-test";
|
||||
match storage.is_supported() {
|
||||
true => {
|
||||
debug!("Using KeyringStorage");
|
||||
(Box::new(storage), app_name)
|
||||
}
|
||||
false => {
|
||||
warn!("KeyringStorage is not supported; using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), "bookmarks")
|
||||
}
|
||||
}
|
||||
};
|
||||
// Make a key storage (wno-keyring)
|
||||
#[cfg(not(feature = "with-keyring"))]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "bookmarks";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "bookmarks-test";
|
||||
debug!("Using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), app_name)
|
||||
};
|
||||
// Get key storage
|
||||
let (key_storage, service_id) = Self::keyring(storage_path, keyring);
|
||||
// Load key
|
||||
let key: String = match key_storage.get_key(service_id) {
|
||||
Ok(k) => {
|
||||
@@ -130,6 +100,37 @@ impl BookmarksClient {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Get the key storage
|
||||
fn keyring(storage_path: &Path, keyring: bool) -> (Box<dyn KeyStorage>, &'static str) {
|
||||
if keyring && cfg!(feature = "keyring") {
|
||||
debug!("Setting up KeyStorage");
|
||||
let username: String = whoami::username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
// Check if keyring storage is supported
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "termscp";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "termscp-test";
|
||||
match storage.is_supported() {
|
||||
true => {
|
||||
debug!("Using KeyringStorage");
|
||||
(Box::new(storage), app_name)
|
||||
}
|
||||
false => {
|
||||
warn!("KeyringStorage is not supported; using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), "bookmarks")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "bookmarks";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "bookmarks-test";
|
||||
debug!("Using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), app_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over bookmarks keys
|
||||
pub fn iter_bookmarks(&self) -> impl Iterator<Item = &String> + '_ {
|
||||
Box::new(self.hosts.bookmarks.keys())
|
||||
@@ -389,7 +390,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Verify client
|
||||
assert_eq!(client.hosts.bookmarks.len(), 0);
|
||||
assert_eq!(client.hosts.recents.len(), 0);
|
||||
@@ -405,7 +406,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add some bookmarks
|
||||
client.add_bookmark(
|
||||
"raspberry",
|
||||
@@ -430,7 +431,7 @@ mod tests {
|
||||
let key: String = client.key.clone();
|
||||
// Re-initialize a client
|
||||
let client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Verify it loaded parameters correctly
|
||||
assert_eq!(client.key, key);
|
||||
let bookmark = ftparams_to_tup(client.get_bookmark("raspberry").unwrap());
|
||||
@@ -453,7 +454,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add s3 bookmark
|
||||
client.add_bookmark("my-bucket", make_s3_ftparams(), true);
|
||||
// Verify bookmark
|
||||
@@ -473,7 +474,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add s3 bookmark
|
||||
client.add_bookmark("my-bucket", make_s3_ftparams(), false);
|
||||
// Verify bookmark
|
||||
@@ -494,7 +495,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add s3 bookmark
|
||||
client.add_recent(make_s3_ftparams());
|
||||
// Verify bookmark
|
||||
@@ -517,7 +518,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"raspberry",
|
||||
@@ -568,7 +569,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"",
|
||||
@@ -589,7 +590,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"raspberry",
|
||||
@@ -617,7 +618,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
@@ -653,7 +654,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
@@ -680,7 +681,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2, true).unwrap();
|
||||
// Add recent, wait 1 second for each one (cause the name depends on time)
|
||||
// 1
|
||||
client.add_recent(make_generic_ftparams(
|
||||
@@ -748,7 +749,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"",
|
||||
@@ -769,7 +770,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
client.key = "MYSUPERSECRETKEY".to_string();
|
||||
assert_eq!(
|
||||
client.decrypt_str("z4Z6LpcpYqBW4+bkIok+5A==").ok().unwrap(),
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
// Locals
|
||||
// Ext
|
||||
use std::fs::{create_dir, remove_file, File, OpenOptions};
|
||||
use std::fs::{File, OpenOptions, create_dir, remove_file};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::config::params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD};
|
||||
use crate::config::serialization::{deserialize, serialize, SerializerError, SerializerErrorKind};
|
||||
use crate::config::params::{DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD, UserConfig};
|
||||
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
|
||||
use crate::explorer::GroupDirs;
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
|
||||
@@ -153,10 +153,7 @@ impl ConfigClient {
|
||||
// Convert string to `GroupDirs`
|
||||
match &self.config.user_interface.group_dirs {
|
||||
None => None,
|
||||
Some(val) => match GroupDirs::from_str(val.as_str()) {
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => None,
|
||||
},
|
||||
Some(val) => GroupDirs::from_str(val.as_str()).ok(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,9 +477,11 @@ mod tests {
|
||||
// Change some stuff
|
||||
client.set_text_editor(PathBuf::from("/usr/bin/vim"));
|
||||
client.set_default_protocol(FileTransferProtocol::Scp);
|
||||
assert!(client
|
||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||
.is_ok());
|
||||
assert!(
|
||||
client
|
||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||
.is_ok()
|
||||
);
|
||||
assert!(client.write_config().is_ok());
|
||||
// Istantiate a new client
|
||||
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
@@ -678,9 +677,11 @@ mod tests {
|
||||
.unwrap();
|
||||
// Add a new key
|
||||
let rsa_key: String = get_sample_rsa_key();
|
||||
assert!(client
|
||||
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
|
||||
.is_ok());
|
||||
assert!(
|
||||
client
|
||||
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
|
||||
.is_ok()
|
||||
);
|
||||
// Iterate keys
|
||||
for key in client.iter_ssh_keys() {
|
||||
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap();
|
||||
|
||||
@@ -76,14 +76,14 @@ impl KeyStorage for KeyringStorage {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use whoami::username;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "isolated-tests"))]
|
||||
#[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))]
|
||||
fn test_system_keys_keyringstorage() {
|
||||
use pretty_assertions::assert_eq;
|
||||
use whoami::username;
|
||||
|
||||
use super::*;
|
||||
|
||||
let username: String = username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
assert!(storage.is_supported());
|
||||
|
||||
@@ -4,29 +4,24 @@
|
||||
|
||||
// Storages
|
||||
pub mod filestorage;
|
||||
#[cfg(feature = "with-keyring")]
|
||||
pub mod keyringstorage;
|
||||
// ext
|
||||
#[cfg(feature = "with-keyring")]
|
||||
use keyring::Error as KeyringError;
|
||||
use thiserror::Error;
|
||||
|
||||
/// defines the error type for the `KeyStorage`
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KeyStorageError {
|
||||
#[cfg(feature = "with-keyring")]
|
||||
#[error("Key has a bad syntax")]
|
||||
BadSytax,
|
||||
#[error("Provider service error")]
|
||||
ProviderError,
|
||||
#[error("No such key")]
|
||||
NoSuchKey,
|
||||
#[cfg(feature = "with-keyring")]
|
||||
#[error("keyring error: {0}")]
|
||||
KeyringError(KeyringError),
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-keyring")]
|
||||
impl From<KeyringError> for KeyStorageError {
|
||||
fn from(e: KeyringError) -> Self {
|
||||
Self::KeyringError(e)
|
||||
@@ -58,7 +53,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_mod_errors() {
|
||||
#[cfg(feature = "with-keyring")]
|
||||
assert_eq!(
|
||||
KeyStorageError::BadSytax.to_string(),
|
||||
String::from("Key has a bad syntax")
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn init(level: LogLevel) -> Result<(), String> {
|
||||
Ok(None) => {
|
||||
return Err(String::from(
|
||||
"This system doesn't seem to support CACHE_DIR",
|
||||
))
|
||||
));
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
@@ -29,6 +29,9 @@ pub fn init(level: LogLevel) -> Result<(), String> {
|
||||
.set_time_format_rfc3339()
|
||||
.add_filter_allow_str("termscp")
|
||||
.add_filter_allow_str("remotefs")
|
||||
.add_filter_allow_str("kube")
|
||||
.add_filter_allow_str("suppaftp")
|
||||
.add_filter_allow_str("pavao")
|
||||
.build();
|
||||
// Make logger
|
||||
WriteLogger::init(level, config, file).map_err(|e| format!("Failed to initialize logger: {e}"))
|
||||
|
||||
@@ -44,15 +44,29 @@ impl SshKeyStorage {
|
||||
/// Resolve host via ssh2 configuration
|
||||
fn resolve_host_in_ssh2_configuration(&self, host: &str) -> Option<PathBuf> {
|
||||
self.ssh_config.as_ref().and_then(|x| {
|
||||
let key = x
|
||||
.query(host)
|
||||
x.query(host)
|
||||
.identity_file
|
||||
.as_ref()
|
||||
.and_then(|x| x.first().cloned());
|
||||
|
||||
key
|
||||
.and_then(|x| x.first().cloned())
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -66,9 +80,13 @@ impl SshKeyStorageTrait for SshKeyStorage {
|
||||
username, host
|
||||
);
|
||||
// otherwise search in configuration
|
||||
let key = self.resolve_host_in_ssh2_configuration(host)?;
|
||||
debug!("Found key in SSH config for {host}: {}", key.display());
|
||||
Some(key)
|
||||
if let Some(key) = self.resolve_host_in_ssh2_configuration(host) {
|
||||
debug!("Found key in SSH config for {host}: {}", key.display());
|
||||
return Some(key);
|
||||
}
|
||||
|
||||
// As a final fallback, try default SSH identity files (like regular ssh does)
|
||||
self.get_default_identity_files().into_iter().next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +141,11 @@ mod tests {
|
||||
.ok()
|
||||
.unwrap();
|
||||
// Add ssh key
|
||||
assert!(client
|
||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||
.is_ok());
|
||||
assert!(
|
||||
client
|
||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||
.is_ok()
|
||||
);
|
||||
// Create ssh key storage
|
||||
let storage: SshKeyStorage = SshKeyStorage::from(&client);
|
||||
// Verify key exists
|
||||
@@ -135,13 +155,21 @@ mod tests {
|
||||
*storage.resolve("192.168.1.31", "pi").unwrap(),
|
||||
exp_key_path
|
||||
);
|
||||
// Verify unexisting key
|
||||
assert!(storage.resolve("deskichup", "veeso").is_none());
|
||||
// Verify key is a default key or 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]
|
||||
fn sould_resolve_key_from_ssh2_config() {
|
||||
let rsa_key = test_helpers::create_sample_file_with_content("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a");
|
||||
let rsa_key = test_helpers::create_sample_file_with_content(
|
||||
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a",
|
||||
);
|
||||
let ssh_config_file = test_helpers::create_sample_file_with_content(format!(
|
||||
r#"
|
||||
Host test
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::fs::OpenOptions;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::config::serialization::{deserialize, serialize, SerializerError, SerializerErrorKind};
|
||||
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
|
||||
use crate::config::themes::Theme;
|
||||
|
||||
/// ThemeProvider provides a high level API to communicate with the termscp theme
|
||||
|
||||
@@ -7,7 +7,7 @@ mod change;
|
||||
// -- export
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
|
||||
use std::sync::mpsc::{Receiver, RecvTimeoutError, channel};
|
||||
use std::time::Duration;
|
||||
|
||||
pub use change::FsChange;
|
||||
@@ -245,9 +245,11 @@ mod test {
|
||||
fn should_watch_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// check if in paths
|
||||
assert_eq!(
|
||||
watcher.paths.get(tempdir.path()).unwrap(),
|
||||
@@ -261,16 +263,20 @@ mod test {
|
||||
fn should_not_watch_path_if_subdir_of_watched_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// watch subdir
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
// should return already watched
|
||||
assert!(watcher
|
||||
.watch(subdir.as_path(), Path::new("/tmp/test/abc/def"))
|
||||
.is_err());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(subdir.as_path(), Path::new("/tmp/test/abc/def"))
|
||||
.is_err()
|
||||
);
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
@@ -279,9 +285,11 @@ mod test {
|
||||
fn should_unwatch_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// unwatch
|
||||
assert!(watcher.unwatch(tempdir.path()).is_ok());
|
||||
assert!(watcher.paths.get(tempdir.path()).is_none());
|
||||
@@ -293,9 +301,11 @@ mod test {
|
||||
fn should_unwatch_path_when_subdir() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// unwatch
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
@@ -318,9 +328,11 @@ mod test {
|
||||
fn should_tell_whether_path_is_watched() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(watcher.watched(tempdir.path()), true);
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
@@ -336,9 +348,11 @@ mod test {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||
assert!(watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// create file
|
||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||
// wait
|
||||
@@ -362,9 +376,11 @@ mod test {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||
assert!(watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// create file
|
||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
@@ -424,9 +440,11 @@ mod test {
|
||||
fn should_poll_nothing() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
assert!(watcher.poll().ok().unwrap().is_none());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
@@ -437,9 +455,11 @@ mod test {
|
||||
fn should_get_watched_paths() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
assert!(watcher.watch(Path::new("/tmp"), Path::new("/tmp")).is_ok());
|
||||
assert!(watcher
|
||||
.watch(Path::new("/home"), Path::new("/home"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(Path::new("/home"), Path::new("/home"))
|
||||
.is_ok()
|
||||
);
|
||||
let mut watched_paths = watcher.watched_paths();
|
||||
watched_paths.sort();
|
||||
assert_eq!(watched_paths, vec![Path::new("/home"), Path::new("/tmp")]);
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
// Locals
|
||||
use super::{AuthActivity, FileTransferParams, FormTab, HostBridgeProtocol};
|
||||
use crate::filetransfer::HostBridgeParams;
|
||||
use crate::filetransfer::params::{
|
||||
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
|
||||
WebDAVProtocolParams,
|
||||
};
|
||||
use crate::filetransfer::HostBridgeParams;
|
||||
|
||||
impl AuthActivity {
|
||||
/// Delete bookmark
|
||||
|
||||
@@ -197,7 +197,7 @@ impl DeleteBookmarkPopup {
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.value(1)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
@@ -265,7 +265,7 @@ impl DeleteRecentPopup {
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.value(1)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
@@ -337,7 +337,7 @@ impl BookmarkSavePassword {
|
||||
.sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.value(0)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
|
||||
@@ -10,14 +10,13 @@ use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
|
||||
use super::{FileTransferProtocol, FormMsg, Msg, UiMsg};
|
||||
use crate::ui::activities::auth::{
|
||||
FormTab, HostBridgeProtocol, UiAuthFormMsg, HOST_BRIDGE_RADIO_PROTOCOL_FTP,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_FTPS, HOST_BRIDGE_RADIO_PROTOCOL_KUBE,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST, HOST_BRIDGE_RADIO_PROTOCOL_S3,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_SCP, HOST_BRIDGE_RADIO_PROTOCOL_SFTP,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_SMB, HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV, REMOTE_RADIO_PROTOCOL_FTP,
|
||||
REMOTE_RADIO_PROTOCOL_FTPS, REMOTE_RADIO_PROTOCOL_KUBE, REMOTE_RADIO_PROTOCOL_S3,
|
||||
REMOTE_RADIO_PROTOCOL_SCP, REMOTE_RADIO_PROTOCOL_SFTP, REMOTE_RADIO_PROTOCOL_SMB,
|
||||
REMOTE_RADIO_PROTOCOL_WEBDAV,
|
||||
FormTab, HOST_BRIDGE_RADIO_PROTOCOL_FTP, HOST_BRIDGE_RADIO_PROTOCOL_FTPS,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_KUBE, HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_S3, HOST_BRIDGE_RADIO_PROTOCOL_SCP, HOST_BRIDGE_RADIO_PROTOCOL_SFTP,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_SMB, HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV, HostBridgeProtocol,
|
||||
REMOTE_RADIO_PROTOCOL_FTP, REMOTE_RADIO_PROTOCOL_FTPS, REMOTE_RADIO_PROTOCOL_KUBE,
|
||||
REMOTE_RADIO_PROTOCOL_S3, REMOTE_RADIO_PROTOCOL_SCP, REMOTE_RADIO_PROTOCOL_SFTP,
|
||||
REMOTE_RADIO_PROTOCOL_SMB, REMOTE_RADIO_PROTOCOL_WEBDAV, UiAuthFormMsg,
|
||||
};
|
||||
|
||||
// -- protocol
|
||||
@@ -37,9 +36,9 @@ impl RemoteProtocolRadio {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(if cfg!(smb) {
|
||||
&["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV", "SMB"]
|
||||
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV", "SMB"].into_iter()
|
||||
} else {
|
||||
&["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"]
|
||||
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"].into_iter()
|
||||
})
|
||||
.foreground(color)
|
||||
.rewind(true)
|
||||
@@ -93,10 +92,10 @@ impl Component<Msg, NoUserEvent> for RemoteProtocolRadio {
|
||||
code: Key::Down, ..
|
||||
}) => return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ProtocolBlurDown))),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ProtocolBlurUp)))
|
||||
return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ProtocolBlurUp)));
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur)))
|
||||
return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur)));
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::BackTab, ..
|
||||
@@ -127,7 +126,7 @@ impl HostBridgeProtocolRadio {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(if cfg!(smb) {
|
||||
&[
|
||||
vec![
|
||||
"Localhost",
|
||||
"SFTP",
|
||||
"SCP",
|
||||
@@ -138,8 +137,9 @@ impl HostBridgeProtocolRadio {
|
||||
"WebDAV",
|
||||
"SMB",
|
||||
]
|
||||
.into_iter()
|
||||
} else {
|
||||
&[
|
||||
vec![
|
||||
"Localhost",
|
||||
"SFTP",
|
||||
"SCP",
|
||||
@@ -149,6 +149,7 @@ impl HostBridgeProtocolRadio {
|
||||
"Kube",
|
||||
"WebDAV",
|
||||
]
|
||||
.into_iter()
|
||||
})
|
||||
.foreground(color)
|
||||
.rewind(true)
|
||||
@@ -228,10 +229,10 @@ impl Component<Msg, NoUserEvent> for HostBridgeProtocolRadio {
|
||||
code: Key::Down, ..
|
||||
}) => return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurDown))),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurUp)))
|
||||
return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurUp)));
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur)))
|
||||
return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur)));
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::BackTab, ..
|
||||
@@ -650,7 +651,7 @@ impl RadioS3NewPathStyle {
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.foreground(color)
|
||||
.rewind(true)
|
||||
.title("New path style", Alignment::Left)
|
||||
|
||||
@@ -28,7 +28,7 @@ impl ErrorPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ impl InfoPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ impl WaitPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ impl WindowSizeError {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(
|
||||
.text([TextSpan::from(
|
||||
"termscp requires at least 24 lines of height to run",
|
||||
)])
|
||||
.wrap(true),
|
||||
@@ -163,7 +163,7 @@ impl QuitPopup {
|
||||
.foreground(color)
|
||||
.title("Quit termscp?", Alignment::Center)
|
||||
.rewind(true)
|
||||
.choices(&["Yes", "No"]),
|
||||
.choices(["Yes", "No"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,7 @@ impl InstallUpdatePopup {
|
||||
.foreground(color)
|
||||
.title("Install update?", Alignment::Center)
|
||||
.rewind(true)
|
||||
.choices(&["Yes", "No"]),
|
||||
.choices(["Yes", "No"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,13 +296,7 @@ impl ReleaseNotes {
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Release notes", Alignment::Center)
|
||||
.text_rows(
|
||||
notes
|
||||
.lines()
|
||||
.map(TextSpan::from)
|
||||
.collect::<Vec<TextSpan>>()
|
||||
.as_slice(),
|
||||
),
|
||||
.text_rows(notes.lines().map(TextSpan::from)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ pub struct NewVersionDisclaimer {
|
||||
impl NewVersionDisclaimer {
|
||||
pub fn new(new_version: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().foreground(color).spans(&[
|
||||
component: Span::default().foreground(color).spans([
|
||||
TextSpan::from("termscp "),
|
||||
TextSpan::new(new_version).underlined().bold(),
|
||||
TextSpan::from(
|
||||
@@ -91,7 +91,7 @@ pub struct HelpFooter {
|
||||
impl HelpFooter {
|
||||
pub fn new(key_color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().spans(&[
|
||||
component: Span::default().spans([
|
||||
TextSpan::from("<F1|CTRL+H>").bold().fg(key_color),
|
||||
TextSpan::from(" Help "),
|
||||
TextSpan::from("<CTRL+C>").bold().fg(key_color),
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
use std::env;
|
||||
|
||||
use super::{AuthActivity, FileTransferParams, FileTransferProtocol, FormTab, HostBridgeProtocol};
|
||||
use crate::filetransfer::params::ProtocolParams;
|
||||
use crate::filetransfer::HostBridgeParams;
|
||||
use crate::filetransfer::params::ProtocolParams;
|
||||
use crate::system::auto_update::{Release, Update, UpdateStatus};
|
||||
use crate::system::notifications::Notification;
|
||||
|
||||
@@ -83,8 +83,16 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
fn collect_localhost_host_params(&self) -> Result<HostBridgeParams, &'static str> {
|
||||
// get remote local path
|
||||
let remote_local_path = self.get_input_local_directory(FormTab::Remote);
|
||||
|
||||
// Local path is:
|
||||
// - the input local path if set
|
||||
// - the remote local path if set
|
||||
// - the current directory if neither is set
|
||||
let path = self
|
||||
.get_input_local_directory(FormTab::HostBridge)
|
||||
.or(remote_local_path)
|
||||
.unwrap_or_else(|| env::current_dir().unwrap_or_default());
|
||||
|
||||
Ok(HostBridgeParams::Localhost(path))
|
||||
|
||||
@@ -17,7 +17,7 @@ use tuirealm::application::PollStrategy;
|
||||
use tuirealm::listener::EventListenerCfg;
|
||||
use tuirealm::{Application, NoUserEvent, Update};
|
||||
|
||||
use super::{Activity, Context, ExitReason, CROSSTERM_MAX_POLL};
|
||||
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -167,7 +167,9 @@ impl FileTransferActivity {
|
||||
}
|
||||
} else {
|
||||
// Do not synchronize, disable sync browsing and return
|
||||
trace!("The user doesn't want to create the directory; disabling synchronized browsing");
|
||||
trace!(
|
||||
"The user doesn't want to create the directory; disabling synchronized browsing"
|
||||
);
|
||||
self.log(
|
||||
LogLevel::Warn,
|
||||
format!("Refused to create '{name}'; synchronized browsing disabled"),
|
||||
|
||||
@@ -18,14 +18,15 @@ impl FileTransferActivity {
|
||||
self.local_copy_file(&entry, dest_path.as_path());
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Try to copy each file to Input/{FILE_NAME}
|
||||
let base_path: PathBuf = PathBuf::from(input);
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
let mut dest_path: PathBuf = base_path.clone();
|
||||
for (entry, mut dest_path) in entries.into_iter() {
|
||||
dest_path.push(entry.name());
|
||||
self.local_copy_file(entry, dest_path.as_path());
|
||||
self.local_copy_file(&entry, dest_path.as_path());
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
@@ -39,14 +40,15 @@ impl FileTransferActivity {
|
||||
self.remote_copy_file(entry, dest_path.as_path());
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Try to copy each file to Input/{FILE_NAME}
|
||||
let base_path: PathBuf = PathBuf::from(input);
|
||||
// Iter files
|
||||
for entry in entries.into_iter() {
|
||||
let mut dest_path: PathBuf = base_path.clone();
|
||||
for (entry, mut dest_path) in entries.into_iter() {
|
||||
dest_path.push(entry.name());
|
||||
self.remote_copy_file(entry, dest_path.as_path());
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,14 @@ impl FileTransferActivity {
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
for (entry, _) in entries.iter() {
|
||||
// Delete file
|
||||
self.local_remove_file(entry);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
@@ -33,10 +37,14 @@ impl FileTransferActivity {
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
for (entry, _) in entries.iter() {
|
||||
// Delete file
|
||||
self.remote_remove_file(entry);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use remotefs::fs::Metadata;
|
||||
use remotefs::File;
|
||||
use remotefs::fs::Metadata;
|
||||
|
||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||
|
||||
@@ -16,7 +16,7 @@ impl FileTransferActivity {
|
||||
pub(crate) fn action_edit_local_file(&mut self) {
|
||||
let entries: Vec<File> = match self.get_local_selected_entries() {
|
||||
SelectedFile::One(entry) => vec![entry],
|
||||
SelectedFile::Many(entries) => entries,
|
||||
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||
SelectedFile::None => vec![],
|
||||
};
|
||||
// Edit all entries
|
||||
@@ -38,12 +38,16 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
}
|
||||
|
||||
pub(crate) fn action_edit_remote_file(&mut self) {
|
||||
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
||||
SelectedFile::One(entry) => vec![entry],
|
||||
SelectedFile::Many(entries) => entries,
|
||||
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||
SelectedFile::None => vec![],
|
||||
};
|
||||
// Edit all entries
|
||||
@@ -60,6 +64,10 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
|
||||
/// Edit a file on localhost
|
||||
@@ -221,10 +229,7 @@ impl FileTransferActivity {
|
||||
/// Edit file on remote host
|
||||
fn edit_remote_file(&mut self, file: File) -> Result<(), String> {
|
||||
// Create temp file
|
||||
let tmpfile: PathBuf = match self.download_file_as_temp(&file) {
|
||||
Ok(p) => p,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let tmpfile = self.download_file_as_temp(&file)?;
|
||||
// Download file
|
||||
let file_name = file.name();
|
||||
let file_path = file.path().to_path_buf();
|
||||
@@ -243,7 +248,7 @@ impl FileTransferActivity {
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.as_path().display(),
|
||||
err
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
// Edit file
|
||||
@@ -256,7 +261,7 @@ impl FileTransferActivity {
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.as_path().display(),
|
||||
err
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
// Check if file has changed
|
||||
@@ -282,7 +287,7 @@ impl FileTransferActivity {
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.as_path().display(),
|
||||
err
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
// Send file
|
||||
|
||||
@@ -2,41 +2,127 @@
|
||||
//!
|
||||
//! `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
|
||||
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 {
|
||||
pub(crate) fn action_local_exec(&mut self, input: String) {
|
||||
match self.host_bridge.exec(input.as_str()) {
|
||||
Ok(output) => {
|
||||
// Reload files
|
||||
self.log(LogLevel::Info, format!("\"{input}\": {output}"));
|
||||
}
|
||||
self.action_exec(false, input);
|
||||
}
|
||||
|
||||
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) => {
|
||||
// Report err
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not execute command \"{input}\": {err}"),
|
||||
);
|
||||
self.log(LogLevel::Error, format!("Invalid command: {err}"));
|
||||
self.print_terminal(err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
match self.client.as_mut().exec(input.as_str()) {
|
||||
Ok((rc, output)) => {
|
||||
// Reload files
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("\"{input}\" (exitcode: {rc}): {output}"),
|
||||
);
|
||||
fn action_exec_exit(&mut self) {
|
||||
self.browser.toggle_terminal(false);
|
||||
self.umount_exec();
|
||||
}
|
||||
|
||||
fn action_exec_cd(&mut self, remote: bool, input: String) {
|
||||
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) => {
|
||||
// Report err
|
||||
self.log_and_alert(
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!("Could not execute command \"{input}\": {err}"),
|
||||
format!("Could not execute command \"{cmd}\": {err}"),
|
||||
);
|
||||
self.print_terminal(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use regex::Regex;
|
||||
use remotefs::File;
|
||||
use wildmatch::WildMatch;
|
||||
|
||||
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
||||
use crate::ui::activities::filetransfer::FileTransferActivity;
|
||||
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Filter {
|
||||
|
||||
@@ -11,7 +11,7 @@ use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, Tr
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_find_changedir(&mut self) {
|
||||
// Match entry
|
||||
if let SelectedFile::One(entry) = self.get_found_selected_entries() {
|
||||
if let Some(entry) = self.get_found_selected_file() {
|
||||
debug!("Changedir to: {}", entry.name());
|
||||
// Get path: if a directory, use directory path; if it is a File, get parent path
|
||||
let path = if entry.is_dir() {
|
||||
@@ -103,11 +103,12 @@ impl FileTransferActivity {
|
||||
// Check which file would be replaced
|
||||
let existing_files: Vec<&File> = entries
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
.filter(|(x, dest_path)| {
|
||||
self.remote_file_exists(
|
||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
||||
)
|
||||
})
|
||||
.map(|(x, _)| x)
|
||||
.collect();
|
||||
// Check whether to replace files
|
||||
if !existing_files.is_empty()
|
||||
@@ -117,7 +118,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
if let Err(err) = self.filetransfer_send(
|
||||
TransferPayload::Many(entries),
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
@@ -134,11 +135,12 @@ impl FileTransferActivity {
|
||||
// Check which file would be replaced
|
||||
let existing_files: Vec<&File> = entries
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
.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()
|
||||
@@ -148,7 +150,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
if let Err(err) = self.filetransfer_recv(
|
||||
TransferPayload::Many(entries),
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
@@ -157,6 +159,12 @@ impl FileTransferActivity {
|
||||
format!("Could not download file: {err}"),
|
||||
);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
if let Some(f) = self.found_mut() {
|
||||
f.clear_queue();
|
||||
self.update_find_list();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,10 +180,16 @@ impl FileTransferActivity {
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
for (entry, _) in entries.iter() {
|
||||
// Delete file
|
||||
self.remove_found_file(entry);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
if let Some(f) = self.found_mut() {
|
||||
f.clear_queue();
|
||||
self.update_find_list();
|
||||
}
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
@@ -200,10 +214,15 @@ impl FileTransferActivity {
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
for (entry, _) in entries.iter() {
|
||||
// Open file
|
||||
self.open_found_file(entry, None);
|
||||
}
|
||||
// clear selection
|
||||
if let Some(f) = self.found_mut() {
|
||||
f.clear_queue();
|
||||
self.update_find_list();
|
||||
}
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
@@ -217,10 +236,15 @@ impl FileTransferActivity {
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
for (entry, _) in entries.iter() {
|
||||
// Open file
|
||||
self.open_found_file(entry, Some(with));
|
||||
}
|
||||
// clear selection
|
||||
if let Some(f) = self.found_mut() {
|
||||
f.clear_queue();
|
||||
self.update_find_list();
|
||||
}
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
|
||||
19
src/ui/activities/filetransfer/actions/mark.rs
Normal file
19
src/ui/activities/filetransfer/actions/mark.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
use super::FileTransferActivity;
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_mark_file(&mut self, index: usize) {
|
||||
self.enqueue_file(index);
|
||||
}
|
||||
|
||||
pub(crate) fn action_mark_all(&mut self) {
|
||||
self.enqueue_all();
|
||||
}
|
||||
|
||||
pub(crate) fn action_mark_clear(&mut self) {
|
||||
self.clear_queue();
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,19 @@
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
use remotefs::fs::UnixPex;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use remotefs::File;
|
||||
use remotefs::fs::UnixPex;
|
||||
use tuirealm::{State, StateValue};
|
||||
|
||||
use super::browser::FileExplorerTab;
|
||||
use super::lib::browser::FoundExplorerTab;
|
||||
use super::{
|
||||
FileTransferActivity, Id, LogLevel, Msg, PendingActionMsg, TransferMsg, TransferOpts,
|
||||
TransferPayload, UiMsg,
|
||||
};
|
||||
use crate::explorer::FileExplorer;
|
||||
|
||||
// actions
|
||||
pub(crate) mod change_dir;
|
||||
@@ -21,6 +25,7 @@ pub(crate) mod edit;
|
||||
pub(crate) mod exec;
|
||||
pub(crate) mod filter;
|
||||
pub(crate) mod find;
|
||||
pub(crate) mod mark;
|
||||
pub(crate) mod mkdir;
|
||||
pub(crate) mod newfile;
|
||||
pub(crate) mod open;
|
||||
@@ -36,7 +41,8 @@ pub(crate) mod watcher;
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SelectedFile {
|
||||
One(File),
|
||||
Many(Vec<File>),
|
||||
/// List of file with their destination path
|
||||
Many(Vec<(File, PathBuf)>),
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -45,7 +51,10 @@ impl SelectedFile {
|
||||
/// In case is `Many` the first item mode is returned
|
||||
pub fn unix_pex(&self) -> Option<UnixPex> {
|
||||
match self {
|
||||
Self::Many(files) => files.iter().next().and_then(|file| file.metadata().mode),
|
||||
Self::Many(files) => files
|
||||
.iter()
|
||||
.next()
|
||||
.and_then(|(file, _)| file.metadata().mode),
|
||||
Self::One(file) => file.metadata().mode,
|
||||
Self::None => None,
|
||||
}
|
||||
@@ -55,7 +64,7 @@ impl SelectedFile {
|
||||
pub fn get_files(self) -> Vec<File> {
|
||||
match self {
|
||||
Self::One(file) => vec![file],
|
||||
Self::Many(files) => files,
|
||||
Self::Many(files) => files.into_iter().map(|(f, _)| f).collect(),
|
||||
Self::None => vec![],
|
||||
}
|
||||
}
|
||||
@@ -64,7 +73,6 @@ impl SelectedFile {
|
||||
#[derive(Debug)]
|
||||
enum SelectedFileIndex {
|
||||
One(usize),
|
||||
Many(Vec<usize>),
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -77,68 +85,42 @@ impl From<Option<&File>> for SelectedFile {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<&File>> for SelectedFile {
|
||||
fn from(files: Vec<&File>) -> Self {
|
||||
SelectedFile::Many(files.into_iter().cloned().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Get local file entry
|
||||
pub(crate) fn get_local_selected_entries(&self) -> SelectedFile {
|
||||
match self.get_selected_index(&Id::ExplorerHostBridge) {
|
||||
SelectedFileIndex::One(idx) => SelectedFile::from(self.host_bridge().get(idx)),
|
||||
SelectedFileIndex::Many(files) => {
|
||||
let files: Vec<&File> = files
|
||||
.iter()
|
||||
.filter_map(|x| self.host_bridge().get(*x)) // Usize to Option<File>
|
||||
.collect();
|
||||
SelectedFile::from(files)
|
||||
}
|
||||
SelectedFileIndex::None => SelectedFile::None,
|
||||
}
|
||||
pub(crate) fn get_local_selected_entries(&mut self) -> SelectedFile {
|
||||
self.get_selected_files(&Id::ExplorerHostBridge)
|
||||
}
|
||||
|
||||
pub(crate) fn get_local_selected_file(&self) -> Option<File> {
|
||||
self.get_selected_file(&Id::ExplorerHostBridge)
|
||||
}
|
||||
|
||||
/// Get remote file entry
|
||||
pub(crate) fn get_remote_selected_entries(&self) -> SelectedFile {
|
||||
match self.get_selected_index(&Id::ExplorerRemote) {
|
||||
SelectedFileIndex::One(idx) => SelectedFile::from(self.remote().get(idx)),
|
||||
SelectedFileIndex::Many(files) => {
|
||||
let files: Vec<&File> = files
|
||||
.iter()
|
||||
.filter_map(|x| self.remote().get(*x)) // Usize to Option<File>
|
||||
.collect();
|
||||
SelectedFile::from(files)
|
||||
}
|
||||
SelectedFileIndex::None => SelectedFile::None,
|
||||
}
|
||||
pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile {
|
||||
self.get_selected_files(&Id::ExplorerRemote)
|
||||
}
|
||||
|
||||
pub(crate) fn get_remote_selected_file(&self) -> Option<File> {
|
||||
self.get_selected_file(&Id::ExplorerRemote)
|
||||
}
|
||||
|
||||
/// Returns whether only one entry is selected on local host
|
||||
pub(crate) fn is_local_selected_one(&self) -> bool {
|
||||
pub(crate) fn is_local_selected_one(&mut self) -> bool {
|
||||
matches!(self.get_local_selected_entries(), SelectedFile::One(_))
|
||||
}
|
||||
|
||||
/// Returns whether only one entry is selected on remote host
|
||||
pub(crate) fn is_remote_selected_one(&self) -> bool {
|
||||
pub(crate) fn is_remote_selected_one(&mut self) -> bool {
|
||||
matches!(self.get_remote_selected_entries(), SelectedFile::One(_))
|
||||
}
|
||||
|
||||
/// Get remote file entry
|
||||
pub(crate) fn get_found_selected_entries(&self) -> SelectedFile {
|
||||
match self.get_selected_index(&Id::ExplorerFind) {
|
||||
SelectedFileIndex::One(idx) => {
|
||||
SelectedFile::from(self.found().as_ref().unwrap().get(idx))
|
||||
}
|
||||
SelectedFileIndex::Many(files) => {
|
||||
let files: Vec<&File> = files
|
||||
.iter()
|
||||
.filter_map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option<File>
|
||||
.collect();
|
||||
SelectedFile::from(files)
|
||||
}
|
||||
SelectedFileIndex::None => SelectedFile::None,
|
||||
}
|
||||
pub(crate) fn get_found_selected_entries(&mut self) -> SelectedFile {
|
||||
self.get_selected_files(&Id::ExplorerFind)
|
||||
}
|
||||
|
||||
pub(crate) fn get_found_selected_file(&self) -> Option<File> {
|
||||
self.get_selected_file(&Id::ExplorerFind)
|
||||
}
|
||||
|
||||
// -- private
|
||||
@@ -146,17 +128,69 @@ impl FileTransferActivity {
|
||||
fn get_selected_index(&self, id: &Id) -> SelectedFileIndex {
|
||||
match self.app.state(id) {
|
||||
Ok(State::One(StateValue::Usize(idx))) => SelectedFileIndex::One(idx),
|
||||
Ok(State::Vec(files)) => {
|
||||
let list: Vec<usize> = files
|
||||
.iter()
|
||||
.map(|x| match x {
|
||||
StateValue::Usize(v) => *v,
|
||||
_ => 0,
|
||||
})
|
||||
.collect();
|
||||
SelectedFileIndex::Many(list)
|
||||
}
|
||||
_ => SelectedFileIndex::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_files(&mut self, id: &Id) -> SelectedFile {
|
||||
let browser = self.browser_by_id(id);
|
||||
// if transfer queue is not empty, return that
|
||||
let transfer_queue = browser.enqueued().clone();
|
||||
if !transfer_queue.is_empty() {
|
||||
return SelectedFile::Many(
|
||||
transfer_queue
|
||||
.iter()
|
||||
.filter_map(|(src, dest)| {
|
||||
let src_file = self.get_file_from_path(id, src)?;
|
||||
Some((src_file, dest.clone()))
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
let browser = self.browser_by_id(id);
|
||||
// if no transfer queue, return selected files
|
||||
match self.get_selected_index(id) {
|
||||
SelectedFileIndex::One(idx) => {
|
||||
let Some(f) = browser.get(idx) else {
|
||||
return SelectedFile::None;
|
||||
};
|
||||
SelectedFile::One(f.clone())
|
||||
}
|
||||
SelectedFileIndex::None => SelectedFile::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_from_path(&mut self, id: &Id, path: &Path) -> Option<File> {
|
||||
match *id {
|
||||
Id::ExplorerHostBridge => self.host_bridge.stat(path).ok(),
|
||||
Id::ExplorerRemote => self.client.stat(path).ok(),
|
||||
Id::ExplorerFind => {
|
||||
let found = self.browser.found_tab().unwrap();
|
||||
match found {
|
||||
FoundExplorerTab::Local => self.host_bridge.stat(path).ok(),
|
||||
FoundExplorerTab::Remote => self.client.stat(path).ok(),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn browser_by_id(&self, id: &Id) -> &FileExplorer {
|
||||
match *id {
|
||||
Id::ExplorerHostBridge => self.host_bridge(),
|
||||
Id::ExplorerRemote => self.remote(),
|
||||
Id::ExplorerFind => self.found().as_ref().unwrap(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_file(&self, id: &Id) -> Option<File> {
|
||||
let browser = self.browser_by_id(id);
|
||||
// if no transfer queue, return selected files
|
||||
match self.get_selected_index(id) {
|
||||
SelectedFileIndex::One(idx) => browser.get(idx).cloned(),
|
||||
SelectedFileIndex::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,32 @@ impl FileTransferActivity {
|
||||
pub(crate) fn action_open_local(&mut self) {
|
||||
let entries: Vec<File> = match self.get_local_selected_entries() {
|
||||
SelectedFile::One(entry) => vec![entry],
|
||||
SelectedFile::Many(entries) => entries,
|
||||
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||
SelectedFile::None => vec![],
|
||||
};
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_local_file(x, None));
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
}
|
||||
|
||||
/// Open local file
|
||||
pub(crate) fn action_open_remote(&mut self) {
|
||||
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
||||
SelectedFile::One(entry) => vec![entry],
|
||||
SelectedFile::Many(entries) => entries,
|
||||
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||
SelectedFile::None => vec![],
|
||||
};
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_remote_file(x, None));
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
|
||||
/// Perform open lopcal file
|
||||
@@ -86,26 +94,33 @@ impl FileTransferActivity {
|
||||
pub(crate) fn action_local_open_with(&mut self, with: &str) {
|
||||
let entries: Vec<File> = match self.get_local_selected_entries() {
|
||||
SelectedFile::One(entry) => vec![entry],
|
||||
SelectedFile::Many(entries) => entries,
|
||||
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||
SelectedFile::None => vec![],
|
||||
};
|
||||
// Open all entries
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_local_file(x, Some(with)));
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
}
|
||||
|
||||
/// Open selected file with provided application
|
||||
pub(crate) fn action_remote_open_with(&mut self, with: &str) {
|
||||
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
||||
SelectedFile::One(entry) => vec![entry],
|
||||
SelectedFile::Many(entries) => entries,
|
||||
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||
SelectedFile::None => vec![],
|
||||
};
|
||||
// Open all entries
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_remote_file(x, Some(with)));
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
|
||||
fn open_bridged_file(&mut self, entry: &File, open_with: Option<&str>) {
|
||||
|
||||
@@ -18,13 +18,15 @@ impl FileTransferActivity {
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Try to copy each file to Input/{FILE_NAME}
|
||||
let base_path: PathBuf = PathBuf::from(input);
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
let mut dest_path: PathBuf = base_path.clone();
|
||||
for (entry, mut dest_path) in entries.into_iter() {
|
||||
dest_path.push(entry.name());
|
||||
self.local_rename_file(entry, dest_path.as_path());
|
||||
self.local_rename_file(&entry, dest_path.as_path());
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
@@ -38,13 +40,16 @@ impl FileTransferActivity {
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Try to copy each file to Input/{FILE_NAME}
|
||||
let base_path: PathBuf = PathBuf::from(input);
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
let mut dest_path: PathBuf = base_path.clone();
|
||||
for (entry, mut dest_path) in entries.into_iter() {
|
||||
dest_path.push(entry.name());
|
||||
self.remote_rename_file(entry, dest_path.as_path());
|
||||
self.remote_rename_file(&entry, dest_path.as_path());
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
// reload remote
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
|
||||
@@ -64,11 +64,12 @@ impl FileTransferActivity {
|
||||
// Check which file would be replaced
|
||||
let existing_files: Vec<&File> = entries
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
.filter(|(x, dest_path)| {
|
||||
self.remote_file_exists(
|
||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
||||
)
|
||||
})
|
||||
.map(|(x, _)| x)
|
||||
.collect();
|
||||
// Check whether to replace files
|
||||
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
|
||||
@@ -76,7 +77,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
if let Err(err) = self.filetransfer_send(
|
||||
TransferPayload::Many(entries),
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
@@ -86,6 +87,10 @@ impl FileTransferActivity {
|
||||
format!("Could not upload file: {err}"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
}
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
@@ -128,11 +133,12 @@ impl FileTransferActivity {
|
||||
// Check which file would be replaced
|
||||
let existing_files: Vec<&File> = entries
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
.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) {
|
||||
@@ -140,7 +146,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
if let Err(err) = self.filetransfer_recv(
|
||||
TransferPayload::Many(entries),
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
@@ -150,6 +156,11 @@ impl FileTransferActivity {
|
||||
format!("Could not download file: {err}"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
// reload remote
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
// locals
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{FileTransferActivity, LogLevel, SelectedFile};
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Create symlink on localhost
|
||||
pub(crate) fn action_local_symlink(&mut self, name: String) {
|
||||
if let SelectedFile::One(entry) = self.get_local_selected_entries() {
|
||||
if let Some(entry) = self.get_local_selected_file() {
|
||||
match self
|
||||
.host_bridge
|
||||
.symlink(PathBuf::from(name.as_str()).as_path(), entry.path())
|
||||
@@ -34,7 +34,7 @@ impl FileTransferActivity {
|
||||
|
||||
/// Copy file on remote
|
||||
pub(crate) fn action_remote_symlink(&mut self, name: String) {
|
||||
if let SelectedFile::One(entry) = self.get_remote_selected_entries() {
|
||||
if let Some(entry) = self.get_remote_selected_file() {
|
||||
match self
|
||||
.client
|
||||
.symlink(PathBuf::from(name.as_str()).as_path(), entry.path())
|
||||
|
||||
@@ -53,12 +53,20 @@ impl MockComponent for Log {
|
||||
.unwrap()
|
||||
.unwrap_table()
|
||||
.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();
|
||||
let title = ("Log".to_string(), Alignment::Left);
|
||||
let w = TuiList::new(list_items)
|
||||
.block(tui_realm_stdlib::utils::get_block(
|
||||
borders,
|
||||
Some(("Log".to_string(), Alignment::Left)),
|
||||
Some(&title),
|
||||
focus,
|
||||
None,
|
||||
))
|
||||
@@ -166,6 +174,12 @@ impl Component<Msg, NoUserEvent> for Log {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => Some(Msg::Ui(UiMsg::BottomPanelRight)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => Some(Msg::Ui(UiMsg::BottomPanelLeft)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct FooterBar {
|
||||
impl FooterBar {
|
||||
pub fn new(key_color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().spans(&[
|
||||
component: Span::default().spans([
|
||||
TextSpan::from("<F1|H>").bold().fg(key_color),
|
||||
TextSpan::from(" Help "),
|
||||
TextSpan::from("<TAB>").bold().fg(key_color),
|
||||
|
||||
@@ -12,20 +12,24 @@ use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
|
||||
mod log;
|
||||
mod misc;
|
||||
mod popups;
|
||||
mod selected_files;
|
||||
mod terminal;
|
||||
mod transfer;
|
||||
|
||||
pub use misc::FooterBar;
|
||||
pub use popups::{
|
||||
ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup,
|
||||
ATTR_FILES, ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, FatalPopup,
|
||||
FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
||||
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
||||
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
|
||||
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList,
|
||||
WatcherPopup, ATTR_FILES,
|
||||
WatcherPopup,
|
||||
};
|
||||
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
|
||||
|
||||
pub use self::log::Log;
|
||||
pub use self::selected_files::SelectedFilesList;
|
||||
pub use self::terminal::Terminal;
|
||||
|
||||
#[derive(Default, MockComponent)]
|
||||
pub struct GlobalListener {
|
||||
|
||||
@@ -20,7 +20,7 @@ use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
use uzers::{get_group_by_gid, get_user_by_uid};
|
||||
|
||||
pub use self::chmod::ChmodPopup;
|
||||
pub use self::goto::{GotoPopup, ATTR_FILES};
|
||||
pub use self::goto::{ATTR_FILES, GotoPopup};
|
||||
use super::super::Browser;
|
||||
use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
|
||||
use crate::explorer::FileSorting;
|
||||
@@ -214,7 +214,7 @@ impl DeletePopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.value(1)
|
||||
.title("Delete file(s)?", Alignment::Center),
|
||||
}
|
||||
@@ -279,7 +279,7 @@ impl DisconnectPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.title("Are you sure you want to disconnect?", Alignment::Center),
|
||||
}
|
||||
}
|
||||
@@ -344,7 +344,7 @@ impl ErrorPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.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)]
|
||||
pub struct FatalPopup {
|
||||
component: Paragraph,
|
||||
@@ -461,7 +378,7 @@ impl FatalPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
@@ -497,6 +414,10 @@ impl FileInfoPopup {
|
||||
texts
|
||||
.add_col(TextSpan::from("Path: "))
|
||||
.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() {
|
||||
texts
|
||||
.add_row()
|
||||
@@ -670,7 +591,7 @@ impl KeybindingsPopup {
|
||||
))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<P>").bold().fg(key_color))
|
||||
.add_col(TextSpan::from(" Toggle log panel"))
|
||||
.add_col(TextSpan::from(" Toggle bottom panel"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<Q|F10>").bold().fg(key_color))
|
||||
.add_col(TextSpan::from(" Quit termscp"))
|
||||
@@ -1121,7 +1042,7 @@ impl QuitPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.title("Are you sure you want to quit termscp?", Alignment::Center),
|
||||
}
|
||||
}
|
||||
@@ -1275,7 +1196,7 @@ impl ReplacePopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.title(text, Alignment::Center),
|
||||
}
|
||||
}
|
||||
@@ -1502,7 +1423,7 @@ impl SortingPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.choices(&["Name", "Modify time", "Creation time", "Size"])
|
||||
.choices(["Name", "Modify time", "Creation time", "Size"])
|
||||
.title("Sort files by…", Alignment::Center)
|
||||
.value(match value {
|
||||
FileSorting::CreationTime => 2,
|
||||
@@ -1554,7 +1475,7 @@ impl StatusBarLocal {
|
||||
let file_sorting = file_sorting_label(browser.host_bridge().file_sorting);
|
||||
let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible());
|
||||
Self {
|
||||
component: Span::default().spans(&[
|
||||
component: Span::default().spans([
|
||||
TextSpan::new("File sorting: ").fg(sorting_color),
|
||||
TextSpan::new(file_sorting).fg(sorting_color).reversed(),
|
||||
TextSpan::new(" Hidden files: ").fg(hidden_color),
|
||||
@@ -1589,7 +1510,7 @@ impl StatusBarRemote {
|
||||
false => "OFF",
|
||||
};
|
||||
Self {
|
||||
component: Span::default().spans(&[
|
||||
component: Span::default().spans([
|
||||
TextSpan::new("File sorting: ").fg(sorting_color),
|
||||
TextSpan::new(file_sorting).fg(sorting_color).reversed(),
|
||||
TextSpan::new(" Hidden files: ").fg(hidden_color),
|
||||
@@ -1728,7 +1649,7 @@ impl SyncBrowsingMkdirPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.title(
|
||||
format!(
|
||||
r#"Sync browsing: directory "{dir_name}" doesn't exist. Do you want to create it?"#
|
||||
@@ -1802,7 +1723,7 @@ impl WaitPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
@@ -1830,7 +1751,7 @@ impl WalkdirWaitPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[
|
||||
.text([
|
||||
TextSpan::from(text.as_ref()),
|
||||
TextSpan::from("Press 'CTRL+C' to abort"),
|
||||
])
|
||||
@@ -1961,7 +1882,7 @@ impl WatcherPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.title(text, Alignment::Center),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,21 +59,21 @@ impl ChmodPopup {
|
||||
},
|
||||
user: Checkbox::default()
|
||||
.foreground(color)
|
||||
.choices(&["Read", "Write", "Execute"])
|
||||
.choices(["Read", "Write", "Execute"])
|
||||
.title("User", Alignment::Left)
|
||||
.borders(Borders::default().sides(BorderSides::NONE))
|
||||
.values(&make_pex_values(pex.user()))
|
||||
.rewind(true),
|
||||
group: Checkbox::default()
|
||||
.foreground(color)
|
||||
.choices(&["Read", "Write", "Execute"])
|
||||
.choices(["Read", "Write", "Execute"])
|
||||
.title("Group", Alignment::Left)
|
||||
.borders(Borders::default().sides(BorderSides::NONE))
|
||||
.values(&make_pex_values(pex.group()))
|
||||
.rewind(true),
|
||||
others: Checkbox::default()
|
||||
.foreground(color)
|
||||
.choices(&["Read", "Write", "Execute"])
|
||||
.choices(["Read", "Write", "Execute"])
|
||||
.title("Others", Alignment::Left)
|
||||
.borders(Borders::default().sides(BorderSides::NONE))
|
||||
.values(&make_pex_values(pex.others()))
|
||||
@@ -208,9 +208,11 @@ impl MockComponent for ChmodPopup {
|
||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
||||
.unwrap_flag();
|
||||
|
||||
let div_title = (self.title.clone(), Alignment::Center);
|
||||
|
||||
let div = tui_realm_stdlib::utils::get_block(
|
||||
Borders::default().color(self.color),
|
||||
Some((self.title.clone(), Alignment::Center)),
|
||||
Some(&div_title),
|
||||
focus,
|
||||
None,
|
||||
);
|
||||
|
||||
126
src/ui/activities/filetransfer/components/selected_files.rs
Normal file
126
src/ui/activities/filetransfer/components/selected_files.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use tui_realm_stdlib::List;
|
||||
use tuirealm::command::{Cmd, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent};
|
||||
use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
|
||||
use crate::ui::activities::filetransfer::{MarkQueue, Msg, UiMsg};
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct SelectedFilesList {
|
||||
component: List,
|
||||
paths: Vec<PathBuf>,
|
||||
queue: MarkQueue,
|
||||
}
|
||||
|
||||
impl SelectedFilesList {
|
||||
pub fn new(
|
||||
paths: &[(PathBuf, PathBuf)],
|
||||
queue: MarkQueue,
|
||||
color: Color,
|
||||
title: &'static str,
|
||||
) -> Self {
|
||||
let enqueued_paths = paths
|
||||
.iter()
|
||||
.map(|(src, _)| src.clone())
|
||||
.collect::<Vec<PathBuf>>();
|
||||
|
||||
Self {
|
||||
queue,
|
||||
paths: enqueued_paths,
|
||||
component: List::default()
|
||||
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
||||
.rewind(true)
|
||||
.scroll(true)
|
||||
.step(4)
|
||||
.highlighted_color(color)
|
||||
.highlighted_str("➤ ")
|
||||
.title(title, Alignment::Left)
|
||||
.rows(
|
||||
paths
|
||||
.iter()
|
||||
.map(|(src, dest)| {
|
||||
vec![
|
||||
TextSpan::from(Self::filename(src)),
|
||||
TextSpan::from(" -> "),
|
||||
TextSpan::from(Self::filename(dest)),
|
||||
]
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn filename(p: &Path) -> String {
|
||||
p.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for SelectedFilesList {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
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)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => Some(Msg::Ui(UiMsg::BottomPanelRight)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => Some(Msg::Ui(UiMsg::BottomPanelLeft)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::BackTab | Key::Tab | Key::Char('p'),
|
||||
..
|
||||
}) => Some(Msg::Ui(UiMsg::LogBackTabbed)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter | Key::Delete,
|
||||
..
|
||||
}) => {
|
||||
// unmark the selected file
|
||||
let State::One(StateValue::Usize(idx)) = self.state() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let path = self.paths.get(idx)?;
|
||||
|
||||
Some(Msg::Ui(UiMsg::MarkRemove(self.queue, path.clone())))
|
||||
}
|
||||
_ => 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 ','
|
||||
}
|
||||
}
|
||||
@@ -17,20 +17,17 @@ const PROP_DOT_DOT: &str = "dot_dot";
|
||||
/// OwnStates contains states for this component
|
||||
#[derive(Clone, Default)]
|
||||
struct OwnStates {
|
||||
list_index: usize, // Index of selected element in list
|
||||
selected: Vec<usize>, // Selected files
|
||||
list_index: usize, // Index of selected element in list
|
||||
list_len: usize, // Length of the list
|
||||
dot_dot: bool,
|
||||
}
|
||||
|
||||
impl OwnStates {
|
||||
/// Initialize list states
|
||||
pub fn init_list_states(&mut self, len: usize, has_dot_dot: bool) {
|
||||
self.selected = Vec::with_capacity(len + if has_dot_dot { 1 } else { 0 });
|
||||
self.list_len = len + if has_dot_dot { 1 } else { 0 };
|
||||
self.fix_list_index();
|
||||
}
|
||||
|
||||
/// Return current value for list index
|
||||
pub fn list_index(&self) -> usize {
|
||||
self.list_index
|
||||
self.dot_dot = has_dot_dot;
|
||||
}
|
||||
|
||||
/// Incremenet list index.
|
||||
@@ -44,6 +41,14 @@ impl OwnStates {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn real_index(&self) -> usize {
|
||||
if self.dot_dot {
|
||||
self.list_index.saturating_sub(1)
|
||||
} else {
|
||||
self.list_index
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrement list index
|
||||
/// If `can_rewind` is `true` the index rewinds when boundary is reached
|
||||
pub fn decr_list_index(&mut self, can_rewind: bool) {
|
||||
@@ -68,22 +73,7 @@ impl OwnStates {
|
||||
|
||||
/// Returns the length of the file list, which is actually the capacity of the selection vector
|
||||
pub fn list_len(&self) -> usize {
|
||||
self.selected.capacity()
|
||||
}
|
||||
|
||||
/// Returns whether the file with index `entry` is selected
|
||||
pub fn is_selected(&self, entry: usize) -> bool {
|
||||
self.selected.contains(&entry)
|
||||
}
|
||||
|
||||
/// Returns whether the selection is currently empty
|
||||
pub fn is_selection_empty(&self) -> bool {
|
||||
self.selected.is_empty()
|
||||
}
|
||||
|
||||
/// Returns current file selection
|
||||
pub fn get_selection(&self) -> Vec<usize> {
|
||||
self.selected.clone()
|
||||
self.list_len
|
||||
}
|
||||
|
||||
/// Keep index if possible, otherwise set to lenght - 1
|
||||
@@ -94,44 +84,6 @@ impl OwnStates {
|
||||
self.list_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// -- select manipulation
|
||||
|
||||
/// Select or deselect file with provided entry index
|
||||
pub fn toggle_file(&mut self, entry: usize) {
|
||||
match self.is_selected(entry) {
|
||||
true => self.deselect(entry),
|
||||
false => self.select(entry),
|
||||
}
|
||||
// increment index
|
||||
self.incr_list_index(false);
|
||||
}
|
||||
|
||||
/// Select all files
|
||||
pub fn select_all(&mut self, has_dot_dot: bool) {
|
||||
for i in 0..self.list_len() {
|
||||
self.select(i + if has_dot_dot { 1 } else { 0 });
|
||||
}
|
||||
}
|
||||
|
||||
/// Select all files
|
||||
pub fn deselect_all(&mut self) {
|
||||
self.selected.clear();
|
||||
}
|
||||
|
||||
/// Select provided index if not selected yet
|
||||
fn select(&mut self, entry: usize) {
|
||||
if !self.is_selected(entry) {
|
||||
self.selected.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove element file with associated index
|
||||
fn deselect(&mut self, entry: usize) {
|
||||
if self.is_selected(entry) {
|
||||
self.selected.retain(|&x| x != entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -206,7 +158,7 @@ impl MockComponent for FileList {
|
||||
.props
|
||||
.get_or(Attribute::Focus, AttrValue::Flag(false))
|
||||
.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
|
||||
let init_table_iter = if self.has_dot_dot() {
|
||||
vec![vec![TextSpan::from("..")]]
|
||||
@@ -222,27 +174,12 @@ impl MockComponent for FileList {
|
||||
Some(table) => init_table_iter
|
||||
.iter()
|
||||
.chain(table.iter())
|
||||
.enumerate()
|
||||
.map(|(num, row)| {
|
||||
let real_num = num;
|
||||
let num = if self.has_dot_dot() {
|
||||
num.checked_sub(1).unwrap_or_default()
|
||||
} else {
|
||||
num
|
||||
};
|
||||
|
||||
.map(|row| {
|
||||
let columns: Vec<Span> = row
|
||||
.iter()
|
||||
.map(|col| {
|
||||
let (fg, bg, mut modifiers) =
|
||||
let (fg, bg, modifiers) =
|
||||
tui_realm_stdlib::utils::use_or_default_styles(&self.props, col);
|
||||
if !(self.has_dot_dot() && real_num == 0)
|
||||
&& self.states.is_selected(num)
|
||||
{
|
||||
modifiers |= TextModifiers::REVERSED
|
||||
| TextModifiers::UNDERLINED
|
||||
| TextModifiers::ITALIC;
|
||||
}
|
||||
|
||||
Span::styled(
|
||||
col.content.clone(),
|
||||
@@ -302,20 +239,11 @@ impl MockComponent for FileList {
|
||||
return State::One(StateValue::String("..".to_string()));
|
||||
}
|
||||
|
||||
match self.states.is_selection_empty() {
|
||||
true => State::One(StateValue::Usize(if self.has_dot_dot() {
|
||||
self.states.list_index.checked_sub(1).unwrap_or_default()
|
||||
} else {
|
||||
self.states.list_index
|
||||
})),
|
||||
false => State::Vec(
|
||||
self.states
|
||||
.get_selection()
|
||||
.into_iter()
|
||||
.map(StateValue::Usize)
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
State::One(StateValue::Usize(if self.has_dot_dot() {
|
||||
self.states.list_index.checked_sub(1).unwrap_or_default()
|
||||
} else {
|
||||
self.states.list_index
|
||||
}))
|
||||
}
|
||||
|
||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||
@@ -374,25 +302,18 @@ impl MockComponent for FileList {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
Cmd::Custom(FILE_LIST_CMD_SELECT_ALL) => {
|
||||
self.states.select_all(self.has_dot_dot());
|
||||
CmdResult::None
|
||||
}
|
||||
Cmd::Custom(FILE_LIST_CMD_DESELECT_ALL) => {
|
||||
self.states.deselect_all();
|
||||
CmdResult::None
|
||||
}
|
||||
Cmd::Toggle => {
|
||||
if self.has_dot_dot() && self.states.list_index() == 0 {
|
||||
if self.states.list_index == 0 && self.has_dot_dot() {
|
||||
return CmdResult::None;
|
||||
}
|
||||
|
||||
self.states.toggle_file(if self.has_dot_dot() {
|
||||
self.states.list_index().checked_sub(1).unwrap_or_default()
|
||||
} else {
|
||||
self.states.list_index()
|
||||
});
|
||||
CmdResult::None
|
||||
let index = self.states.real_index();
|
||||
self.states.list_index = self
|
||||
.states
|
||||
.list_index
|
||||
.saturating_add(1)
|
||||
.min(self.states.list_len.saturating_sub(1));
|
||||
CmdResult::Changed(State::One(StateValue::Usize(index)))
|
||||
}
|
||||
_ => CmdResult::None,
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ impl FileListWithSearch {
|
||||
|
||||
pub fn borders(mut self, b: Borders) -> Self {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ impl ExplorerFuzzy {
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.title(title, Alignment::Left)
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,21 +132,26 @@ impl ExplorerFuzzy {
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
Some(Msg::Ui(UiMsg::MarkAll))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_DESELECT_ALL));
|
||||
Some(Msg::None)
|
||||
Some(Msg::Ui(UiMsg::MarkClear))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
Some(Msg::None)
|
||||
let CmdResult::Changed(State::One(StateValue::Usize(index))) =
|
||||
self.perform(Cmd::Toggle)
|
||||
else {
|
||||
return Some(Msg::None);
|
||||
};
|
||||
|
||||
Some(Msg::Ui(UiMsg::MarkFile(index)))
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
self.perform(Cmd::Change);
|
||||
@@ -231,7 +236,7 @@ impl ExplorerFind {
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.title(title, Alignment::Left)
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,21 +282,26 @@ impl Component<Msg, NoUserEvent> for ExplorerFind {
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
Some(Msg::Ui(UiMsg::MarkAll))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_DESELECT_ALL));
|
||||
Some(Msg::None)
|
||||
Some(Msg::Ui(UiMsg::MarkClear))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
Some(Msg::None)
|
||||
let CmdResult::Changed(State::One(StateValue::Usize(index))) =
|
||||
self.perform(Cmd::Toggle)
|
||||
else {
|
||||
return Some(Msg::None);
|
||||
};
|
||||
|
||||
Some(Msg::Ui(UiMsg::MarkFile(index)))
|
||||
}
|
||||
// -- comp msg
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
@@ -363,7 +373,7 @@ impl ExplorerLocal {
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.title(title, Alignment::Left)
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect())
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect())
|
||||
.dot_dot(true),
|
||||
}
|
||||
}
|
||||
@@ -410,21 +420,26 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
Some(Msg::Ui(UiMsg::MarkAll))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_DESELECT_ALL));
|
||||
Some(Msg::None)
|
||||
Some(Msg::Ui(UiMsg::MarkClear))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
Some(Msg::None)
|
||||
let CmdResult::Changed(State::One(StateValue::Usize(index))) =
|
||||
self.perform(Cmd::Toggle)
|
||||
else {
|
||||
return Some(Msg::None);
|
||||
};
|
||||
|
||||
Some(Msg::Ui(UiMsg::MarkFile(index)))
|
||||
}
|
||||
// -- comp msg
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
@@ -508,7 +523,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('p'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowLogPanel)),
|
||||
}) => Some(Msg::Ui(UiMsg::GoToTransferQueue)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('r') | Key::Function(6),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
@@ -532,7 +547,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('x'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowExecPopup)),
|
||||
}) => Some(Msg::Ui(UiMsg::ShowTerminal)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('y'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
@@ -572,7 +587,7 @@ impl ExplorerRemote {
|
||||
.foreground(fg)
|
||||
.highlighted_color(hg)
|
||||
.title(title, Alignment::Left)
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect())
|
||||
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect())
|
||||
.dot_dot(true),
|
||||
}
|
||||
}
|
||||
@@ -619,21 +634,26 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||
Some(Msg::None)
|
||||
Some(Msg::Ui(UiMsg::MarkAll))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('a'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_DESELECT_ALL));
|
||||
Some(Msg::None)
|
||||
Some(Msg::Ui(UiMsg::MarkClear))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('m'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => {
|
||||
let _ = self.perform(Cmd::Toggle);
|
||||
Some(Msg::None)
|
||||
let CmdResult::Changed(State::One(StateValue::Usize(index))) =
|
||||
self.perform(Cmd::Toggle)
|
||||
else {
|
||||
return Some(Msg::None);
|
||||
};
|
||||
|
||||
Some(Msg::Ui(UiMsg::MarkFile(index)))
|
||||
}
|
||||
// -- comp msg
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
@@ -717,7 +737,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('p'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowLogPanel)),
|
||||
}) => Some(Msg::Ui(UiMsg::GoToTransferQueue)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('r') | Key::Function(6),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
@@ -741,7 +761,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('x'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowExecPopup)),
|
||||
}) => Some(Msg::Ui(UiMsg::ShowTerminal)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('y'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
|
||||
@@ -60,6 +60,23 @@ impl Browser {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn other_explorer_no_found(&self) -> &FileExplorer {
|
||||
match self.tab {
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.remote,
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.host_bridge,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn explorer_mut(&mut self) -> &mut FileExplorer {
|
||||
match self.tab {
|
||||
FileExplorerTab::HostBridge => &mut self.host_bridge,
|
||||
FileExplorerTab::Remote => &mut self.remote,
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
self.found.as_mut().map(|x| &mut x.explorer).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host_bridge(&self) -> &FileExplorer {
|
||||
&self.host_bridge
|
||||
}
|
||||
@@ -131,6 +148,25 @@ impl Browser {
|
||||
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
|
||||
pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer {
|
||||
let mut builder = Self::build_explorer(cli);
|
||||
|
||||
@@ -3,7 +3,8 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use bytesize::ByteSize;
|
||||
use tuirealm::props::{
|
||||
Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextSpan,
|
||||
Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextModifiers,
|
||||
TextSpan,
|
||||
};
|
||||
use tuirealm::{PollStrategy, Update};
|
||||
|
||||
@@ -90,7 +91,9 @@ impl FileTransferActivity {
|
||||
|
||||
/// Set text editor to use
|
||||
pub(super) fn setup_text_editor(&self) {
|
||||
env::set_var("EDITOR", self.config().get_text_editor());
|
||||
unsafe {
|
||||
env::set_var("EDITOR", self.config().get_text_editor());
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a path to absolute according to host explorer
|
||||
@@ -225,7 +228,7 @@ impl FileTransferActivity {
|
||||
transfer_stats
|
||||
)
|
||||
}
|
||||
TransferPayload::Many(entries) => {
|
||||
TransferPayload::TransferQueue(entries) => {
|
||||
format!(
|
||||
"{} files has been successfully transferred ({})",
|
||||
entries.len(),
|
||||
@@ -238,6 +241,11 @@ impl FileTransferActivity {
|
||||
/// Update host bridge file list
|
||||
pub(super) fn update_host_bridge_filelist(&mut self) {
|
||||
self.reload_host_bridge_dir();
|
||||
self.reload_host_bridge_filelist();
|
||||
}
|
||||
|
||||
/// Update host bridge file list
|
||||
pub(super) fn reload_host_bridge_filelist(&mut self) {
|
||||
// Get width
|
||||
let width = self
|
||||
.context_mut()
|
||||
@@ -259,31 +267,108 @@ impl FileTransferActivity {
|
||||
let files: Vec<Vec<TextSpan>> = self
|
||||
.host_bridge()
|
||||
.iter_files()
|
||||
.map(|x| vec![TextSpan::from(self.host_bridge().fmt_file(x))])
|
||||
.map(|x| {
|
||||
let mut span = TextSpan::from(self.host_bridge().fmt_file(x));
|
||||
if self.host_bridge().enqueued().contains_key(x.path()) {
|
||||
span.modifiers |=
|
||||
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC;
|
||||
}
|
||||
|
||||
vec![span]
|
||||
})
|
||||
.collect();
|
||||
// Update content and title
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerHostBridge,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerHostBridge,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((hostname, Alignment::Left))
|
||||
)
|
||||
.is_ok());
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::ExplorerHostBridge,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::ExplorerHostBridge,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((hostname, Alignment::Left))
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
/// Update remote file list
|
||||
pub(super) fn update_remote_filelist(&mut self) {
|
||||
self.reload_remote_dir();
|
||||
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) {
|
||||
let width = self
|
||||
.context_mut()
|
||||
.terminal()
|
||||
@@ -304,25 +389,35 @@ impl FileTransferActivity {
|
||||
let files: Vec<Vec<TextSpan>> = self
|
||||
.remote()
|
||||
.iter_files()
|
||||
.map(|x| vec![TextSpan::from(self.remote().fmt_file(x))])
|
||||
.map(|x| {
|
||||
let mut span = TextSpan::from(self.remote().fmt_file(x));
|
||||
if self.remote().enqueued().contains_key(x.path()) {
|
||||
span.modifiers |=
|
||||
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC;
|
||||
}
|
||||
|
||||
vec![span]
|
||||
})
|
||||
.collect();
|
||||
// Update content and title
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerRemote,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerRemote,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((hostname, Alignment::Left))
|
||||
)
|
||||
.is_ok());
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::ExplorerRemote,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::ExplorerRemote,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((hostname, Alignment::Left))
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
/// Update log box
|
||||
@@ -361,61 +456,67 @@ impl FileTransferActivity {
|
||||
.add_col(TextSpan::from("]: "))
|
||||
.add_col(TextSpan::from(record.msg.as_str()));
|
||||
}
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::Log,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(table.build())
|
||||
)
|
||||
.is_ok());
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::Log,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(table.build())
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn update_progress_bar(&mut self, filename: String) {
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarFull,
|
||||
Attribute::Text,
|
||||
AttrValue::String(self.transfer.full.to_string())
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarFull,
|
||||
Attribute::Value,
|
||||
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
||||
self.transfer.full.calc_progress()
|
||||
)))
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Text,
|
||||
AttrValue::String(self.transfer.partial.to_string())
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Value,
|
||||
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
||||
self.transfer.partial.calc_progress()
|
||||
)))
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((filename, Alignment::Center))
|
||||
)
|
||||
.is_ok());
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::ProgressBarFull,
|
||||
Attribute::Text,
|
||||
AttrValue::String(self.transfer.full.to_string())
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::ProgressBarFull,
|
||||
Attribute::Value,
|
||||
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
||||
self.transfer.full.calc_progress()
|
||||
)))
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Text,
|
||||
AttrValue::String(self.transfer.partial.to_string())
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Value,
|
||||
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
||||
self.transfer.partial.calc_progress()
|
||||
)))
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::ProgressBarPartial,
|
||||
Attribute::Title,
|
||||
AttrValue::Title((filename, Alignment::Center))
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
/// Finalize find process
|
||||
@@ -448,16 +549,24 @@ impl FileTransferActivity {
|
||||
.found()
|
||||
.unwrap()
|
||||
.iter_files()
|
||||
.map(|x| vec![TextSpan::from(self.found().unwrap().fmt_file(x))])
|
||||
.map(|x| {
|
||||
let mut span = TextSpan::from(self.found().unwrap().fmt_file(x));
|
||||
if self.found().unwrap().enqueued().contains_key(x.path()) {
|
||||
span.modifiers |=
|
||||
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC;
|
||||
}
|
||||
vec![span]
|
||||
})
|
||||
.collect();
|
||||
assert!(self
|
||||
.app
|
||||
.attr(
|
||||
&Id::ExplorerFind,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok());
|
||||
assert!(
|
||||
self.app
|
||||
.attr(
|
||||
&Id::ExplorerFind,
|
||||
Attribute::Content,
|
||||
AttrValue::Table(files)
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn update_browser_file_list(&mut self) {
|
||||
@@ -469,6 +578,15 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn reload_browser_file_list(&mut self) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||
self.reload_host_bridge_filelist()
|
||||
}
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.reload_remote_filelist(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn update_browser_file_list_swapped(&mut self) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||
|
||||
@@ -28,7 +28,7 @@ use session::TransferPayload;
|
||||
use tempfile::TempDir;
|
||||
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
||||
|
||||
use super::{Activity, Context, ExitReason, CROSSTERM_MAX_POLL};
|
||||
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::explorer::{FileExplorer, FileSorting};
|
||||
use crate::filetransfer::{
|
||||
@@ -40,6 +40,12 @@ use crate::system::watcher::FsWatcher;
|
||||
|
||||
// -- components
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||
pub enum MarkQueue {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
enum Id {
|
||||
ChmodPopup,
|
||||
@@ -47,7 +53,6 @@ enum Id {
|
||||
DeletePopup,
|
||||
DisconnectPopup,
|
||||
ErrorPopup,
|
||||
ExecPopup,
|
||||
ExplorerFind,
|
||||
ExplorerHostBridge,
|
||||
ExplorerRemote,
|
||||
@@ -74,6 +79,10 @@ enum Id {
|
||||
StatusBarRemote,
|
||||
SymlinkPopup,
|
||||
SyncBrowsingMkdirPopup,
|
||||
TerminalHostBridge,
|
||||
TerminalRemote,
|
||||
TransferQueueHostBridge,
|
||||
TransferQueueRemote,
|
||||
WaitPopup,
|
||||
WatchedPathsList,
|
||||
WatcherPopup,
|
||||
@@ -125,6 +134,8 @@ enum TransferMsg {
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum UiMsg {
|
||||
BottomPanelLeft,
|
||||
BottomPanelRight,
|
||||
ChangeFileSorting(FileSorting),
|
||||
ChangeTransferWindow,
|
||||
CloseChmodPopup,
|
||||
@@ -153,19 +164,26 @@ enum UiMsg {
|
||||
FilterFiles(String),
|
||||
FuzzySearch(String),
|
||||
LogBackTabbed,
|
||||
/// Mark file on the list; usize is the index of the file
|
||||
MarkFile(usize),
|
||||
MarkRemove(MarkQueue, PathBuf),
|
||||
/// Mark all file at tab
|
||||
MarkAll,
|
||||
/// Clear all marks
|
||||
MarkClear,
|
||||
Quit,
|
||||
ReplacePopupTabbed,
|
||||
ShowChmodPopup,
|
||||
ShowCopyPopup,
|
||||
ShowDeletePopup,
|
||||
ShowDisconnectPopup,
|
||||
ShowExecPopup,
|
||||
ShowTerminal,
|
||||
ShowFileInfoPopup,
|
||||
ShowFileSortingPopup,
|
||||
ShowFilterPopup,
|
||||
ShowGotoPopup,
|
||||
ShowKeybindingsPopup,
|
||||
ShowLogPanel,
|
||||
GoToTransferQueue,
|
||||
ShowMkdirPopup,
|
||||
ShowNewFilePopup,
|
||||
ShowOpenWithPopup,
|
||||
@@ -243,14 +261,14 @@ impl FileTransferActivity {
|
||||
host_bridge_params: HostBridgeParams,
|
||||
remote_params: &FileTransferParams,
|
||||
ticks: Duration,
|
||||
) -> Self {
|
||||
) -> Result<Self, String> {
|
||||
// Get config client
|
||||
let config_client: ConfigClient = Self::init_config_client();
|
||||
// init host bridge
|
||||
let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client);
|
||||
let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client)?;
|
||||
let host_bridge_connected = host_bridge.is_localhost();
|
||||
let enable_fs_watcher = host_bridge.is_localhost();
|
||||
Self {
|
||||
Ok(Self {
|
||||
exit_reason: None,
|
||||
context: None,
|
||||
app: Application::init(
|
||||
@@ -264,15 +282,12 @@ impl FileTransferActivity {
|
||||
remote_params.protocol,
|
||||
remote_params.params.clone(),
|
||||
&config_client,
|
||||
),
|
||||
)?,
|
||||
browser: Browser::new(&config_client),
|
||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||
walkdir: WalkdirStates::default(),
|
||||
transfer: TransferStates::default(),
|
||||
cache: match TempDir::new() {
|
||||
Ok(d) => Some(d),
|
||||
Err(_) => None,
|
||||
},
|
||||
cache: TempDir::new().ok(),
|
||||
fswatcher: if enable_fs_watcher {
|
||||
FsWatcher::init(Duration::from_secs(5)).ok()
|
||||
} else {
|
||||
@@ -280,7 +295,7 @@ impl FileTransferActivity {
|
||||
},
|
||||
host_bridge_connected,
|
||||
remote_connected: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn host_bridge(&self) -> &FileExplorer {
|
||||
@@ -307,6 +322,45 @@ impl FileTransferActivity {
|
||||
self.browser.found_mut()
|
||||
}
|
||||
|
||||
/// Enqueue a file to be transferred
|
||||
fn enqueue_file(&mut self, index: usize) {
|
||||
let Some(src) = self
|
||||
.browser
|
||||
.explorer()
|
||||
.get(index)
|
||||
.map(|item| item.path().to_path_buf())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if self.browser.explorer().enqueued().contains_key(&src) {
|
||||
debug!("File already marked, unmarking {}", src.display());
|
||||
self.browser.explorer_mut().dequeue(&src);
|
||||
} else {
|
||||
debug!("Marking file {}", src.display());
|
||||
let dest = self.browser.other_explorer_no_found().wrkdir.clone();
|
||||
self.browser.explorer_mut().enqueue(&src, &dest);
|
||||
}
|
||||
self.reload_browser_file_list();
|
||||
self.refresh_host_bridge_transfer_queue();
|
||||
self.refresh_remote_transfer_queue();
|
||||
}
|
||||
|
||||
fn enqueue_all(&mut self) {
|
||||
let dest = self.browser.other_explorer_no_found().wrkdir.clone();
|
||||
self.browser.explorer_mut().enqueue_all(&dest);
|
||||
self.reload_browser_file_list();
|
||||
self.refresh_host_bridge_transfer_queue();
|
||||
self.refresh_remote_transfer_queue();
|
||||
}
|
||||
|
||||
fn clear_queue(&mut self) {
|
||||
self.browser.explorer_mut().clear_queue();
|
||||
self.reload_browser_file_list();
|
||||
self.refresh_host_bridge_transfer_queue();
|
||||
self.refresh_remote_transfer_queue();
|
||||
}
|
||||
|
||||
/// Get file name for a file in cache
|
||||
fn get_cache_tmp_name(&self, name: &str, file_type: Option<&str>) -> Option<String> {
|
||||
self.cache.as_ref().map(|_| {
|
||||
@@ -359,7 +413,6 @@ impl FileTransferActivity {
|
||||
* Keep it clean :)
|
||||
* Use methods instead!
|
||||
*/
|
||||
|
||||
impl Activity for FileTransferActivity {
|
||||
/// `on_create` is the function which must be called to initialize the activity.
|
||||
/// `on_create` must initialize all the data structures used by the activity
|
||||
|
||||
@@ -41,7 +41,8 @@ enum TransferErrorReason {
|
||||
pub(super) enum TransferPayload {
|
||||
File(File),
|
||||
Any(File),
|
||||
Many(Vec<File>),
|
||||
/// List of file with their destination name
|
||||
TransferQueue(Vec<(File, PathBuf)>),
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
@@ -264,8 +265,8 @@ impl FileTransferActivity {
|
||||
TransferPayload::File(ref file) => {
|
||||
self.filetransfer_send_file(file, curr_remote_path, dst_name)
|
||||
}
|
||||
TransferPayload::Many(ref entries) => {
|
||||
self.filetransfer_send_many(entries, curr_remote_path)
|
||||
TransferPayload::TransferQueue(ref entries) => {
|
||||
self.filetransfer_send_transfer_queue(entries)
|
||||
}
|
||||
};
|
||||
// Notify
|
||||
@@ -331,18 +332,17 @@ impl FileTransferActivity {
|
||||
result
|
||||
}
|
||||
|
||||
/// Send many entries to remote
|
||||
fn filetransfer_send_many(
|
||||
/// Send transfer queue entries to remote
|
||||
fn filetransfer_send_transfer_queue(
|
||||
&mut self,
|
||||
entries: &[File],
|
||||
curr_remote_path: &Path,
|
||||
entries: &[(File, PathBuf)],
|
||||
) -> Result<(), String> {
|
||||
// Reset states
|
||||
self.transfer.reset();
|
||||
// Calculate total size of transfer
|
||||
let total_transfer_size: usize = entries
|
||||
.iter()
|
||||
.map(|x| self.get_total_transfer_size_host(x))
|
||||
.map(|(x, _)| self.get_total_transfer_size_host(x))
|
||||
.sum();
|
||||
self.transfer.full.init(total_transfer_size);
|
||||
// Mount progress bar
|
||||
@@ -350,7 +350,7 @@ impl FileTransferActivity {
|
||||
// Send recurse
|
||||
let result = entries
|
||||
.iter()
|
||||
.map(|x| self.filetransfer_send_recurse(x, curr_remote_path, None))
|
||||
.map(|(x, remote)| self.filetransfer_send_recurse(x, remote, None))
|
||||
.find(|x| x.is_err())
|
||||
.unwrap_or(Ok(()));
|
||||
// Umount progress bar
|
||||
@@ -704,8 +704,8 @@ impl FileTransferActivity {
|
||||
self.filetransfer_recv_any(entry, host_bridge_path, dst_name)
|
||||
}
|
||||
TransferPayload::File(ref file) => self.filetransfer_recv_file(file, host_bridge_path),
|
||||
TransferPayload::Many(ref entries) => {
|
||||
self.filetransfer_recv_many(entries, host_bridge_path)
|
||||
TransferPayload::TransferQueue(ref entries) => {
|
||||
self.filetransfer_recv_transfer_queue(entries)
|
||||
}
|
||||
};
|
||||
// Notify
|
||||
@@ -764,18 +764,17 @@ impl FileTransferActivity {
|
||||
result.map_err(|x| x.to_string())
|
||||
}
|
||||
|
||||
/// Send many entries to remote
|
||||
fn filetransfer_recv_many(
|
||||
/// Receive transfer queue from remote
|
||||
fn filetransfer_recv_transfer_queue(
|
||||
&mut self,
|
||||
entries: &[File],
|
||||
curr_remote_path: &Path,
|
||||
entries: &[(File, PathBuf)],
|
||||
) -> Result<(), String> {
|
||||
// Reset states
|
||||
self.transfer.reset();
|
||||
// Calculate total size of transfer
|
||||
let total_transfer_size: usize = entries
|
||||
.iter()
|
||||
.map(|x| self.get_total_transfer_size_remote(x))
|
||||
.map(|(x, _)| self.get_total_transfer_size_remote(x))
|
||||
.sum();
|
||||
self.transfer.full.init(total_transfer_size);
|
||||
// Mount progress bar
|
||||
@@ -783,7 +782,7 @@ impl FileTransferActivity {
|
||||
// Send recurse
|
||||
let result = entries
|
||||
.iter()
|
||||
.map(|x| self.filetransfer_recv_recurse(x, curr_remote_path, None))
|
||||
.map(|(x, path)| self.filetransfer_recv_recurse(x, path, None))
|
||||
.find(|x| x.is_err())
|
||||
.unwrap_or(Ok(()));
|
||||
// Umount progress bar
|
||||
@@ -1239,7 +1238,7 @@ impl FileTransferActivity {
|
||||
None => {
|
||||
return Err(String::from(
|
||||
"Could not create tempfile: cache not available",
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
// Download file
|
||||
|
||||
@@ -8,10 +8,12 @@ use remotefs::fs::File;
|
||||
use tuirealm::props::{AttrValue, Attribute};
|
||||
use tuirealm::{State, StateValue, Update};
|
||||
|
||||
use super::actions::walkdir::WalkdirError;
|
||||
use super::actions::SelectedFile;
|
||||
use super::actions::walkdir::WalkdirError;
|
||||
use super::browser::{FileExplorerTab, FoundExplorerTab};
|
||||
use super::{ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg};
|
||||
use super::{
|
||||
ExitReason, FileTransferActivity, Id, MarkQueue, Msg, TransferMsg, TransferOpts, UiMsg,
|
||||
};
|
||||
|
||||
impl Update<Msg> for FileTransferActivity {
|
||||
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
|
||||
@@ -113,7 +115,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::HostBridge => {
|
||||
if let SelectedFile::One(entry) = self.get_local_selected_entries() {
|
||||
if let Some(entry) = self.get_local_selected_file() {
|
||||
self.action_submit_local(entry);
|
||||
// Update file list if sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
@@ -123,7 +125,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => {
|
||||
if let SelectedFile::One(entry) = self.get_remote_selected_entries() {
|
||||
if let Some(entry) = self.get_remote_selected_file() {
|
||||
self.action_submit_remote(entry);
|
||||
// Update file list if sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
@@ -144,17 +146,12 @@ impl FileTransferActivity {
|
||||
self.update_browser_file_list()
|
||||
}
|
||||
TransferMsg::ExecuteCmd(cmd) => {
|
||||
// Exex command
|
||||
self.umount_exec();
|
||||
self.mount_blocking_wait(format!("Executing '{cmd}'…").as_str());
|
||||
// Exec command
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_local_exec(cmd),
|
||||
FileExplorerTab::Remote => self.action_remote_exec(cmd),
|
||||
_ => panic!("Found tab doesn't support EXEC"),
|
||||
}
|
||||
self.umount_wait();
|
||||
// Reload files
|
||||
self.update_browser_file_list()
|
||||
};
|
||||
}
|
||||
TransferMsg::GoTo(dir) => {
|
||||
match self.browser.tab() {
|
||||
@@ -415,7 +412,10 @@ impl FileTransferActivity {
|
||||
UiMsg::CloseDeletePopup => self.umount_radio_delete(),
|
||||
UiMsg::CloseDisconnectPopup => self.umount_disconnect(),
|
||||
UiMsg::CloseErrorPopup => self.umount_error(),
|
||||
UiMsg::CloseExecPopup => self.umount_exec(),
|
||||
UiMsg::CloseExecPopup => {
|
||||
self.browser.toggle_terminal(false);
|
||||
self.umount_exec();
|
||||
}
|
||||
UiMsg::CloseFatalPopup => {
|
||||
self.umount_fatal();
|
||||
self.exit_reason = Some(ExitReason::Disconnect);
|
||||
@@ -473,12 +473,33 @@ impl FileTransferActivity {
|
||||
self.browser.fuzzy_search(&needle);
|
||||
self.update_find_list();
|
||||
}
|
||||
UiMsg::ShowLogPanel => {
|
||||
assert!(self.app.active(&Id::Log).is_ok());
|
||||
UiMsg::GoToTransferQueue => {
|
||||
assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok());
|
||||
}
|
||||
UiMsg::LogBackTabbed => {
|
||||
assert!(self.app.active(&Id::ExplorerHostBridge).is_ok());
|
||||
}
|
||||
UiMsg::MarkFile(index) => {
|
||||
self.action_mark_file(index);
|
||||
}
|
||||
UiMsg::MarkAll => {
|
||||
self.action_mark_all();
|
||||
}
|
||||
UiMsg::MarkClear => {
|
||||
self.action_mark_clear();
|
||||
}
|
||||
UiMsg::MarkRemove(tab, path) => match tab {
|
||||
MarkQueue::Local => {
|
||||
self.host_bridge_mut().dequeue(&path);
|
||||
self.reload_host_bridge_filelist();
|
||||
self.refresh_host_bridge_transfer_queue();
|
||||
}
|
||||
MarkQueue::Remote => {
|
||||
self.remote_mut().dequeue(&path);
|
||||
self.reload_remote_filelist();
|
||||
self.refresh_remote_transfer_queue();
|
||||
}
|
||||
},
|
||||
UiMsg::Quit => {
|
||||
self.disconnect_and_quit();
|
||||
self.umount_quit();
|
||||
@@ -523,7 +544,10 @@ impl FileTransferActivity {
|
||||
UiMsg::ShowCopyPopup => self.mount_copy(),
|
||||
UiMsg::ShowDeletePopup => self.mount_radio_delete(),
|
||||
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 => {
|
||||
if let SelectedFile::One(file) = self.get_local_selected_entries() {
|
||||
self.mount_file_info(&file);
|
||||
@@ -584,6 +608,31 @@ impl FileTransferActivity {
|
||||
UiMsg::WindowResized => {
|
||||
self.redraw = true;
|
||||
}
|
||||
|
||||
UiMsg::BottomPanelLeft => match self.app.focus() {
|
||||
Some(Id::TransferQueueHostBridge) => {
|
||||
assert!(self.app.active(&Id::Log).is_ok())
|
||||
}
|
||||
Some(Id::TransferQueueRemote) => {
|
||||
assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok())
|
||||
}
|
||||
Some(Id::Log) => {
|
||||
assert!(self.app.active(&Id::TransferQueueRemote).is_ok())
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
UiMsg::BottomPanelRight => match self.app.focus() {
|
||||
Some(Id::TransferQueueHostBridge) => {
|
||||
assert!(self.app.active(&Id::TransferQueueRemote).is_ok())
|
||||
}
|
||||
Some(Id::TransferQueueRemote) => {
|
||||
assert!(self.app.active(&Id::Log).is_ok())
|
||||
}
|
||||
Some(Id::Log) => {
|
||||
assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok())
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user