Merged 0.5.0 into main
@@ -28,6 +28,11 @@ A clear and concise description of what you expected to happen.
|
|||||||
- Protocol used
|
- Protocol used
|
||||||
- Remote server version and name
|
- Remote server version and name
|
||||||
|
|
||||||
|
## Log
|
||||||
|
|
||||||
|
Report the snippet of the log file containing the unexpected behaviour.
|
||||||
|
If there is any information you consider to be confidential, shadow it.
|
||||||
|
|
||||||
## Additional information
|
## Additional information
|
||||||
|
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
branch: false
|
||||||
|
ignore-not-existing: true
|
||||||
|
llvm: true
|
||||||
|
output-type: lcov
|
||||||
|
ignore:
|
||||||
|
- "/*"
|
||||||
|
- "C:/*"
|
||||||
|
- "../*"
|
||||||
|
- src/main.rs
|
||||||
|
- src/lib.rs
|
||||||
|
- src/activity_manager.rs
|
||||||
|
- "src/ui/activities/*"
|
||||||
|
- src/ui/context.rs
|
||||||
|
- src/ui/input.rs
|
||||||
@@ -12,11 +12,25 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup rust toolchain
|
||||||
- name: Run cargo-tarpaulin
|
uses: actions-rs/toolchain@v1
|
||||||
uses: actions-rs/tarpaulin@v0.1
|
|
||||||
with:
|
with:
|
||||||
args: "--ignore-tests -- --test-threads 1"
|
toolchain: nightly
|
||||||
|
override: true
|
||||||
- name: Upload to codecov.io
|
- name: Run tests
|
||||||
uses: codecov/codecov-action@v1
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --all-features --no-fail-fast
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: "0"
|
||||||
|
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
|
||||||
|
RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
|
||||||
|
- name: Coverage with grcov
|
||||||
|
id: coverage
|
||||||
|
uses: actions-rs/grcov@v0.1
|
||||||
|
- name: Coveralls
|
||||||
|
uses: coverallsapp/github-action@v1.1.1
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
path-to-lcov: ${{ steps.coverage.outputs.report }}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
- [Changelog](#changelog)
|
- [Changelog](#changelog)
|
||||||
|
- [0.5.0](#050)
|
||||||
- [0.4.2](#042)
|
- [0.4.2](#042)
|
||||||
- [0.4.1](#041)
|
- [0.4.1](#041)
|
||||||
- [0.4.0](#040)
|
- [0.4.0](#040)
|
||||||
@@ -16,6 +17,54 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
Released on 23/05/2021
|
||||||
|
|
||||||
|
> 🌸 Spring Update 2021 🌷
|
||||||
|
|
||||||
|
- **Synchronized browsing**:
|
||||||
|
- Added the possibility to enabled the synchronized brower navigation
|
||||||
|
- when you enter a directory, the same directory will be entered on the other tab
|
||||||
|
- Enable sync browser with `<Y>`
|
||||||
|
- Read more on manual: [Synchronized browsing](docs/man.md#Synchronized-browsing-)
|
||||||
|
- **Remote and Local hosts file formatter**:
|
||||||
|
- Added the possibility to set different formatters for local and remote hosts
|
||||||
|
- **Work on multiple files**:
|
||||||
|
- Added the possibility to work on **multiple files simultaneously**
|
||||||
|
- Select a file with `<M>`, the file when selected will have a `*` prepended to its name
|
||||||
|
- Select all files in the current directory with `<CTRL+A>`
|
||||||
|
- Read more on manual: [Work on multiple files](docs/man.md#Work-on-multiple-files-)
|
||||||
|
- **Logging**:
|
||||||
|
- termscp now writes a log file, useful to debug and to contribute to fix issues.
|
||||||
|
- Read more on [manual](docs/man.md)
|
||||||
|
- **File transfer changes**
|
||||||
|
- *SFTP*
|
||||||
|
- Added **COPY** command to SFTP (Please note that Copy command is not supported by SFTP natively, so here it just uses the `cp` shell command as it does in SCP).
|
||||||
|
- *FTP*
|
||||||
|
- Added support for file copy (achieved through *tricky-copy*: the file is first downloaded, then uploaded with a different file name)
|
||||||
|
- **Double progress bar**:
|
||||||
|
- From now one two progress bar will be displayed:
|
||||||
|
- the first, on top, displays the full transfer state (e.g. when downloading a directory of 10 files, the progress of the entire transfer)
|
||||||
|
- the second, on bottom, displays the transfer of the individual file being written (as happened for the old versions)
|
||||||
|
- changed the progress bar colour from `LightGreen` to `Green`
|
||||||
|
- Enhancements
|
||||||
|
- Added a status bar in the file explorer showing whether the sync browser is enabled and which file sorting mode is selected
|
||||||
|
- Removed the goold old figlet title
|
||||||
|
- Protocol input as first field in UI
|
||||||
|
- Port is now updated to standard for selected protocol
|
||||||
|
- when you change the protocol in the authentication form and the current port is standard (`< 1024`), the port will be automatically changed to default value for the selected protocol (e.g. current port: `123`, protocol changed to `FTP`, port becomes `21`)
|
||||||
|
- Bugfix:
|
||||||
|
- Fixed wrong text wrap in log box
|
||||||
|
- Fixed empty bookmark name causing termscp to crash
|
||||||
|
- Fixed error message not being shown after an upload failure
|
||||||
|
- Fixed default protocol not being loaded from config
|
||||||
|
- [Issue 23](https://github.com/veeso/termscp/issues/23): Remove created file if transfer failed or was abrupted
|
||||||
|
- Dependencies:
|
||||||
|
- Added `tui-realm 0.3.0`
|
||||||
|
- Removed `tui` (as direct dependency)
|
||||||
|
- Updated `regex` to `1.5.4`
|
||||||
|
|
||||||
## 0.4.2
|
## 0.4.2
|
||||||
|
|
||||||
Released on 13/04/2021
|
Released on 13/04/2021
|
||||||
@@ -88,7 +137,7 @@ Released on 28/02/2021
|
|||||||
- Added `EXTRA` and `LENGTH` parameters to format keys.
|
- Added `EXTRA` and `LENGTH` parameters to format keys.
|
||||||
- Now keys are provided with this syntax `{KEY_NAME[:LEN[:EXTRA]}`
|
- Now keys are provided with this syntax `{KEY_NAME[:LEN[:EXTRA]}`
|
||||||
- **Check for updates**:
|
- **Check for updates**:
|
||||||
- TermSCP will now check for updates on startup and will show in the main page if there is a new version available
|
- termscp will now check for updates on startup and will show in the main page if there is a new version available
|
||||||
- This feature may be disabled from setup (Check for updates => No)
|
- This feature may be disabled from setup (Check for updates => No)
|
||||||
- Enhancements:
|
- Enhancements:
|
||||||
- Default choice for deleting file set to "NO" (way too easy to delete files by mistake)
|
- Default choice for deleting file set to "NO" (way too easy to delete files by mistake)
|
||||||
|
|||||||
@@ -19,16 +19,16 @@ Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in
|
|||||||
|
|
||||||
## Project mission
|
## 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. 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
|
||||||
|
|
||||||
> 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".
|
> 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.
|
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.
|
A lot of things have changed since them, both the features the project provides and my personal view of this project.
|
||||||
|
|
||||||
Today I don't see TermSCP as a WinSCP clone anymore. I've also thought about changing the name as the time passed by, but I liked it and it would be hard to change the name on the registries, etc.
|
Today I don't see termscp as a WinSCP clone anymore. I've also thought about changing the name as the time passed by, but I liked it and it would be hard to change the name on the registries, etc.
|
||||||
|
|
||||||
Right now I see TermSCP as a **rich-featured file transfer client for terminals**. All I want is to provide all the features users need to use it correctly, I want it to be **safe and reliable** and eventually I want people to consider termscp **the first choice as a file transfer client**.
|
Right now I see termscp as a **rich-featured file transfer client for terminals**. All I want is to provide all the features users need to use it correctly, I want it to be **safe and reliable** and eventually I want people to consider termscp **the first choice as a file transfer client**.
|
||||||
|
|
||||||
### Project goals
|
### Project goals
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ Don't set other labels to your issue, not even priority.
|
|||||||
|
|
||||||
When you open a bug try to be the most precise as possible in describing your issue. I'm not saying you should always be that precise, since sometimes it's very easy for maintainers to understand what you're talking about. Just try to be reasonable to understand sometimes we might not know what you're talking about or we just don't have the technical knowledge you might think.
|
When you open a bug try to be the most precise as possible in describing your issue. I'm not saying you should always be that precise, since sometimes it's very easy for maintainers to understand what you're talking about. Just try to be reasonable to understand sometimes we might not know what you're talking about or we just don't have the technical knowledge you might think.
|
||||||
Please always provide the environment you're working on and consider that we don't provide any support for older version of termscp, at least for those not classified as LTS (if we'll ever have them).
|
Please always provide the environment you're working on and consider that we don't provide any support for older version of termscp, at least for those not classified as LTS (if we'll ever have them).
|
||||||
|
If you can, provide the log file or the snippet involving your issue. You can find in the [user manual](docs/man.md) the location of the log file.
|
||||||
Last but not least: the template I've written must be used. Full stop.
|
Last but not least: the template I've written must be used. Full stop.
|
||||||
|
|
||||||
Maintainers will may add additional labels to your issue:
|
Maintainers will may add additional labels to your issue:
|
||||||
@@ -68,7 +69,7 @@ Maintainers will may add additional labels to your issue:
|
|||||||
- **duplicate**: the issue is duplicated; the reference to the related issue will be added to your description. Your issue will be closed.
|
- **duplicate**: the issue is duplicated; the reference to the related issue will be added to your description. Your issue will be closed.
|
||||||
- **priority**: this must be fixed asap
|
- **priority**: this must be fixed asap
|
||||||
- **sorcery**: it is not possible to find out what's causing your bug, nor is reproducible on our test environments.
|
- **sorcery**: it is not possible to find out what's causing your bug, nor is reproducible on our test environments.
|
||||||
- **wontfix**: your bug has a very high ratio between the probability to encounter it and the difficult to fix it, or it just isn't a bug, but a feature.
|
- **wontfix**: your bug has a very high ratio between the difficulty to fix it and the probability to encounter it, or it just isn't a bug, but a feature.
|
||||||
|
|
||||||
### Feature requests
|
### Feature requests
|
||||||
|
|
||||||
|
|||||||
@@ -33,24 +33,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.7.15"
|
version = "0.7.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
|
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "ansi_term"
|
||||||
version = "0.3.6"
|
version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
|
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||||
|
dependencies = [
|
||||||
[[package]]
|
"winapi",
|
||||||
name = "arrayvec"
|
]
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
@@ -70,17 +67,6 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "blake2b_simd"
|
|
||||||
version = "0.5.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"arrayvec",
|
|
||||||
"constant_time_eq",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -186,12 +172,6 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "constant_time_eq"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "content_inspector"
|
name = "content_inspector"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -234,47 +214,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
|
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpuid-bool"
|
name = "cpufeatures"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
|
checksum = "281f563b2c3a0e535ab12d81d3c5859045795256ad269afa7c19542585b68f93"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc-any"
|
name = "crc-any"
|
||||||
version = "2.3.5"
|
version = "2.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3784befdf9469f4d51c69ef0b774f6a99de6bcc655285f746f16e0dd63d9007"
|
checksum = "0d98be01088633be44a2a82b55a96dca49b226d65297428a3c44d33de07528ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"debug-helper",
|
"debug-helper",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-utils"
|
|
||||||
version = "0.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"cfg-if 1.0.0",
|
|
||||||
"lazy_static",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm"
|
|
||||||
version = "0.18.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e86d73f2a0b407b5768d10a8c720cf5d2df49a9efc10ca09176d201ead4b7fb"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"crossterm_winapi 0.6.2",
|
|
||||||
"lazy_static",
|
|
||||||
"libc",
|
|
||||||
"mio",
|
|
||||||
"parking_lot 0.11.1",
|
|
||||||
"signal-hook",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
@@ -282,7 +238,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c"
|
checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"crossterm_winapi 0.7.0",
|
"crossterm_winapi",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
@@ -291,15 +247,6 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm_winapi"
|
|
||||||
version = "0.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c2265c3f8e080075d9b6417aa72293fc71662f34b4af2612d8d1b074d29510db"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm_winapi"
|
name = "crossterm_winapi"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -319,6 +266,16 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctor"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dbus"
|
name = "dbus"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -330,9 +287,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "debug-helper"
|
name = "debug-helper"
|
||||||
version = "0.3.10"
|
version = "0.3.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a8a5bb894f24f42c247f19b25928a88e31867c0f84552c05df41a9dd527435e"
|
checksum = "4460596867846f73bddca51f7403b6a29f5315125be10a1640259b4db5b9494c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "des"
|
name = "des"
|
||||||
@@ -345,6 +302,12 @@ dependencies = [
|
|||||||
"opaque-debug",
|
"opaque-debug",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -356,18 +319,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "3.0.1"
|
version = "3.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff"
|
checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs-sys",
|
"dirs-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-sys"
|
name = "dirs-sys"
|
||||||
version = "0.3.5"
|
version = "0.3.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
|
checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"redox_users",
|
"redox_users",
|
||||||
@@ -501,9 +464,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.2.2"
|
version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21"
|
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matches",
|
"matches",
|
||||||
"unicode-bidi",
|
"unicode-bidi",
|
||||||
@@ -527,9 +490,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.50"
|
version = "0.3.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c"
|
checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@@ -554,9 +517,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.92"
|
version = "0.2.94"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714"
|
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libssh2-sys"
|
name = "libssh2-sys"
|
||||||
@@ -574,9 +537,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libz-sys"
|
name = "libz-sys"
|
||||||
version = "1.1.2"
|
version = "1.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655"
|
checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -595,9 +558,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.3"
|
version = "0.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176"
|
checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
@@ -613,9 +576,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "magic-crypt"
|
name = "magic-crypt"
|
||||||
version = "3.1.7"
|
version = "3.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a7d8d3790b76ab76cc459a707e09009fcd8ef8da8999d7a99c9bb9b9bef8890"
|
checksum = "d3c94f1281833c690f81e6a00c545063b4f034509d3af6d29b58d48e39aa64c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-soft",
|
"aes-soft",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -653,9 +616,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.3.4"
|
version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
|
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
@@ -796,9 +759,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.33"
|
version = "0.10.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a61075b62a23fef5a29815de7536d940aa35ce96d18ce0cc5076272db678a577"
|
checksum = "6d7830286ad6a3973c0f1d9b73738f69c76b739301d0229c4b96501695cbe4c8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
@@ -810,15 +773,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.1.2"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
|
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.61"
|
version = "0.9.63"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f"
|
checksum = "b6b0d6fb7d80f877617dfcb014e605e2b5ab2fb0afdf27935219bb6bd984cb98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -827,6 +790,15 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "output_vt100"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -844,7 +816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
|
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"instant",
|
"instant",
|
||||||
"lock_api 0.4.3",
|
"lock_api 0.4.4",
|
||||||
"parking_lot_core 0.8.3",
|
"parking_lot_core 0.8.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -871,7 +843,7 @@ dependencies = [
|
|||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"instant",
|
"instant",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall 0.2.5",
|
"redox_syscall 0.2.8",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
@@ -900,6 +872,18 @@ version = "0.2.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b"
|
||||||
|
dependencies = [
|
||||||
|
"ansi_term",
|
||||||
|
"ctor",
|
||||||
|
"diff",
|
||||||
|
"output_vt100",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
@@ -1007,29 +991,28 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.5"
|
version = "0.2.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
|
checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.3.5"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
|
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.1.16",
|
"getrandom 0.2.2",
|
||||||
"redox_syscall 0.1.57",
|
"redox_syscall 0.2.8",
|
||||||
"rust-argon2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.4.5"
|
version = "1.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19"
|
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1038,9 +1021,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.6.23"
|
version = "0.6.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548"
|
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remove_dir_all"
|
name = "remove_dir_all"
|
||||||
@@ -1076,23 +1059,11 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust-argon2"
|
|
||||||
version = "0.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
|
|
||||||
dependencies = [
|
|
||||||
"base64",
|
|
||||||
"blake2b_simd",
|
|
||||||
"constant_time_eq",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.19.0"
|
version = "0.19.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b"
|
checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"log",
|
"log",
|
||||||
@@ -1125,9 +1096,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sct"
|
name = "sct"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
|
checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
@@ -1197,18 +1168,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.125"
|
version = "1.0.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
|
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.125"
|
version = "1.0.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
|
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1228,13 +1199,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.9.3"
|
version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de"
|
checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"cpuid-bool",
|
"cpufeatures",
|
||||||
"digest",
|
"digest",
|
||||||
"opaque-debug",
|
"opaque-debug",
|
||||||
]
|
]
|
||||||
@@ -1259,6 +1230,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simplelog"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59d0fe306a0ced1c88a58042dc22fc2ddd000982c26d75f6aa09a394547c41e0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"log",
|
||||||
|
"termcolor",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
@@ -1297,9 +1279,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.68"
|
version = "1.0.72"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87"
|
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1315,20 +1297,29 @@ dependencies = [
|
|||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"libc",
|
"libc",
|
||||||
"rand 0.8.3",
|
"rand 0.8.3",
|
||||||
"redox_syscall 0.2.5",
|
"redox_syscall 0.2.8",
|
||||||
"remove_dir_all",
|
"remove_dir_all",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termscp"
|
name = "termscp"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytesize",
|
"bytesize",
|
||||||
"chrono",
|
"chrono",
|
||||||
"content_inspector",
|
"content_inspector",
|
||||||
"crossterm 0.19.0",
|
"crossterm",
|
||||||
"dirs",
|
"dirs",
|
||||||
"edit",
|
"edit",
|
||||||
"ftp4",
|
"ftp4",
|
||||||
@@ -1336,18 +1327,21 @@ dependencies = [
|
|||||||
"hostname",
|
"hostname",
|
||||||
"keyring",
|
"keyring",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"log",
|
||||||
"magic-crypt",
|
"magic-crypt",
|
||||||
"path-slash",
|
"path-slash",
|
||||||
|
"pretty_assertions",
|
||||||
"rand 0.8.3",
|
"rand 0.8.3",
|
||||||
"regex",
|
"regex",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"serde",
|
"serde",
|
||||||
|
"simplelog",
|
||||||
"ssh2",
|
"ssh2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
"tui",
|
"tuirealm",
|
||||||
"ureq",
|
"ureq",
|
||||||
"users",
|
"users",
|
||||||
"whoami",
|
"whoami",
|
||||||
@@ -1408,9 +1402,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023"
|
checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tinyvec_macros",
|
"tinyvec_macros",
|
||||||
]
|
]
|
||||||
@@ -1432,17 +1426,29 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tui"
|
name = "tui"
|
||||||
version = "0.14.0"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ced152a8e9295a5b168adc254074525c17ac4a83c90b2716274cc38118bddc9"
|
checksum = "861d8f3ad314ede6219bcb2ab844054b1de279ee37a9bc38e3d606f9d3fb2a71"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cassowary",
|
"cassowary",
|
||||||
"crossterm 0.18.2",
|
"crossterm",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tuirealm"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aad880656efa943543c8048a28e1fa1d0ea6b5c4bf7f53636492ef8a49ec681f"
|
||||||
|
dependencies = [
|
||||||
|
"crossterm",
|
||||||
|
"textwrap",
|
||||||
|
"tui",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
@@ -1451,9 +1457,9 @@ checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.4"
|
version = "0.3.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
|
checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matches",
|
"matches",
|
||||||
]
|
]
|
||||||
@@ -1481,9 +1487,9 @@ checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
@@ -1493,9 +1499,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ureq"
|
name = "ureq"
|
||||||
version = "2.1.0"
|
version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fbeb1aabb07378cf0e084971a74f24241273304653184f54cdce113c0d7df1b"
|
checksum = "2475a6781e9bc546e7b64f4013d2f4032c8c6a40fcffd7c6f4ee734a890972ab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"chunked_transfer",
|
"chunked_transfer",
|
||||||
@@ -1511,9 +1517,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.2.1"
|
version = "2.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b"
|
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
@@ -1533,9 +1539,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.11"
|
version = "0.2.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
|
checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
@@ -1557,9 +1563,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.73"
|
version = "0.2.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9"
|
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
@@ -1567,9 +1573,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.73"
|
version = "0.2.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae"
|
checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@@ -1582,9 +1588,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.73"
|
version = "0.2.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f"
|
checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -1592,9 +1598,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.73"
|
version = "0.2.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c"
|
checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1605,15 +1611,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.73"
|
version = "0.2.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489"
|
checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.50"
|
version = "0.3.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be"
|
checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -1650,9 +1656,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.1.1"
|
version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e296f550993cba2c5c3eba5da0fb335562b2fa3d97b7a8ac9dc91f40a3abc70"
|
checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@@ -1660,9 +1666,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wildmatch"
|
name = "wildmatch"
|
||||||
version = "2.0.0"
|
version = "2.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07ae7ce410f81ba679081aac1d4874f3b1c328535b630209aa5b4cdaaf895e20"
|
checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
@@ -1680,6 +1686,15 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ categories = ["command-line-utilities"]
|
|||||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP"
|
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP"
|
||||||
documentation = "https://docs.rs/termscp"
|
documentation = "https://docs.rs/termscp"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
homepage = "https://github.com/veeso/termscp"
|
homepage = "https://veeso.github.io/termscp/"
|
||||||
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||||
keywords = ["scp-client", "sftp-client", "ftp-client", "winscp", "command-line-utility"]
|
keywords = ["scp-client", "sftp-client", "ftp-client", "winscp", "command-line-utility"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "termscp"
|
name = "termscp"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/veeso/termscp"
|
repository = "https://github.com/veeso/termscp"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
|
|
||||||
[package.metadata.rpm]
|
[package.metadata.rpm]
|
||||||
package = "termscp"
|
package = "termscp"
|
||||||
@@ -37,18 +37,24 @@ edit = "0.1.3"
|
|||||||
getopts = "0.2.21"
|
getopts = "0.2.21"
|
||||||
hostname = "0.3.1"
|
hostname = "0.3.1"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
log = "0.4.14"
|
||||||
magic-crypt = "3.1.7"
|
magic-crypt = "3.1.7"
|
||||||
rand = "0.8.3"
|
rand = "0.8.3"
|
||||||
regex = "1.4.5"
|
regex = "1.5.4"
|
||||||
rpassword = "5.0.1"
|
rpassword = "5.0.1"
|
||||||
|
simplelog = "0.10.0"
|
||||||
ssh2 = "0.9.0"
|
ssh2 = "0.9.0"
|
||||||
tempfile = "3.1.0"
|
tempfile = "3.1.0"
|
||||||
textwrap = "0.13.4"
|
textwrap = "0.13.4"
|
||||||
thiserror = "^1.0.0"
|
thiserror = "^1.0.0"
|
||||||
toml = "0.5.8"
|
toml = "0.5.8"
|
||||||
|
tuirealm = { version = "0.3.0", features = [ "with-components" ] }
|
||||||
whoami = "1.1.1"
|
whoami = "1.1.1"
|
||||||
wildmatch = "2.0.0"
|
wildmatch = "2.0.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "0.7.2"
|
||||||
|
|
||||||
[dependencies.ftp4]
|
[dependencies.ftp4]
|
||||||
features = ["secure"]
|
features = ["secure"]
|
||||||
version = "^4.0.2"
|
version = "^4.0.2"
|
||||||
@@ -57,11 +63,6 @@ version = "^4.0.2"
|
|||||||
features = ["derive"]
|
features = ["derive"]
|
||||||
version = "^1.0.0"
|
version = "^1.0.0"
|
||||||
|
|
||||||
[dependencies.tui]
|
|
||||||
default-features = false
|
|
||||||
features = ["crossterm"]
|
|
||||||
version = "0.14.0"
|
|
||||||
|
|
||||||
[dependencies.ureq]
|
[dependencies.ureq]
|
||||||
features = ["json"]
|
features = ["json"]
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
|||||||
@@ -1,44 +1,28 @@
|
|||||||
# TermSCP
|
# termscp
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="/assets/images/termscp.svg" width="256" height="256" />
|
<img src="/assets/images/termscp.svg" width="256" height="256" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT) [](https://github.com/veeso/termscp) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
[](https://opensource.org/licenses/MIT) [](https://github.com/veeso/termscp) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||||
|
|
||||||
[](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions)
|
[](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://coveralls.io/github/veeso/termscp)
|
||||||
|
|
||||||
~ A feature rich terminal file transfer ~
|
<p align="center">~ A feature rich terminal file transfer ~</p>
|
||||||
Developed by Christian Visintin
|
<p align="center">
|
||||||
Current version: 0.4.2 (13/04/2021)
|
<a href="https://veeso.github.io/termscp/" target="_blank">Website</a>
|
||||||
|
·
|
||||||
|
<a href="https://veeso.github.io/termscp/#get-started" target="_blank">Installation</a>
|
||||||
|
·
|
||||||
|
<a href="https://veeso.github.io/termscp/#user-manual" target="_blank">User manual</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">Developed by Christian Visintin</p>
|
||||||
|
<p align="center">Current version: 0.5.0 (23/05/2021)</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [TermSCP](#termscp)
|
## About termscp 🖥
|
||||||
- [About TermSCP 🖥](#about-termscp-)
|
|
||||||
- [Why TermSCP 🤔](#why-termscp-)
|
|
||||||
- [Features 🎁](#features-)
|
|
||||||
- [Installation 🛠](#installation-)
|
|
||||||
- [Cargo 🦀](#cargo-)
|
|
||||||
- [Deb package 📦](#deb-package-)
|
|
||||||
- [RPM package 📦](#rpm-package-)
|
|
||||||
- [AUR Package 🔼](#aur-package-)
|
|
||||||
- [Chocolatey 🍫](#chocolatey-)
|
|
||||||
- [Brew 🍻](#brew-)
|
|
||||||
- [User Manual 🎓](#user-manual-)
|
|
||||||
- [Documentation 📚](#documentation-)
|
|
||||||
- [Known issues 🧻](#known-issues-)
|
|
||||||
- [Upcoming Features 🧪](#upcoming-features-)
|
|
||||||
- [Contributing and issues 🤝🏻](#contributing-and-issues-)
|
|
||||||
- [Changelog ⏳](#changelog-)
|
|
||||||
- [Powered by 🚀](#powered-by-)
|
|
||||||
- [Gallery 🎬](#gallery-)
|
|
||||||
- [Buy me a coffee ☕](#buy-me-a-coffee-)
|
|
||||||
- [License 📃](#license-)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## About TermSCP 🖥
|
|
||||||
|
|
||||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **BSD** and **Windows** compatible and supports SFTP, SCP, FTP and FTPS.
|
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **BSD** and **Windows** compatible and supports SFTP, SCP, FTP and FTPS.
|
||||||
|
|
||||||
@@ -46,125 +30,52 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Why TermSCP 🤔
|
|
||||||
|
|
||||||
It happens quite often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there is midnight commander too, but actually I don't like it very much tbh (and hasn't a decent support for scp).
|
|
||||||
|
|
||||||
## Features 🎁
|
## Features 🎁
|
||||||
|
|
||||||
- 📁 Different communication protocols support
|
- 📁 Different communication protocols support
|
||||||
- SFTP
|
- SFTP
|
||||||
- SCP
|
- SCP
|
||||||
- FTP and FTPS
|
- FTP and FTPS
|
||||||
- 🐧 Compatible with Windows, Linux, BSD and MacOS
|
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
|
||||||
- 🖥 Handy user interface to explore and operate on the remote and on the local machine file system
|
|
||||||
- Create, remove, rename, search, view and edit files
|
- Create, remove, rename, search, view and edit files
|
||||||
- ⭐ Bookmarks and recent connections can be saved to access quickly to your favourite hosts
|
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
|
||||||
- 📝 Supports text editors to view and edit text files
|
- 📝 View and edit text files with your favourite text editor
|
||||||
- 💁 Supports both SFTP/SCP authentication through SSH keys and username/password
|
- 💁 SFTP/SCP authentication through SSH keys and username/password
|
||||||
- ✏ Customizations
|
- 🐧 Compatible with Windows, Linux, BSD and MacOS
|
||||||
|
- ✏ Customizable
|
||||||
- Custom file explorer format
|
- Custom file explorer format
|
||||||
- Customizable text editor
|
- Customizable text editor
|
||||||
- Customizable file sorting
|
- Customizable file sorting
|
||||||
- 🔐 SSH key storage
|
- 🔐 Save your password in your operating system key vault
|
||||||
- 🦀 Written in Rust
|
- 🦀 Rust-powered
|
||||||
- 🤝 Easy to extend with new file transfers protocols
|
- 🤝 Easy to extend with new file transfers protocols
|
||||||
- 👀 Developed keeping an eye on performance
|
- 👀 Developed keeping an eye on performance
|
||||||
- 🦄 Frequent awesome updates
|
- 🦄 Frequent awesome updates
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installation 🛠
|
## Get started 🚀
|
||||||
|
|
||||||
If you're considering to install TermSCP I want to thank you 💜 ! I hope you will enjoy TermSCP!
|
If you're considering to install termscp I want to thank you 💜 ! I hope you will enjoy termscp!
|
||||||
If you want to contribute to this project, don't forget to check out our contribute guide. [Read More](CONTRIBUTING.md)
|
If you want to contribute to this project, don't forget to check out our contribute guide. [Read More](CONTRIBUTING.md)
|
||||||
|
|
||||||
### Cargo 🦀
|
If you are a Linux or a MacOS user this simple shell script will install termscp on your system with a single command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Install termscp through cargo
|
curl --proto '=https' --tlsv1.2 -sSf "https://raw.githubusercontent.com/veeso/termscp/main/install.sh" | sh
|
||||||
cargo install termscp
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Requirements:
|
while if you're a Windows user, you can install termscp with [Chocolatey](https://chocolatey.org/).
|
||||||
|
|
||||||
- Linux
|
For more information or other platforms, please visit [veeso.github.io](https://veeso.github.io/termscp/#get-started) to view all installation methods.
|
||||||
- pkg-config
|
|
||||||
- libssh2
|
|
||||||
- openssl
|
|
||||||
|
|
||||||
### Deb package 📦
|
|
||||||
|
|
||||||
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.4.2_amd64.deb)
|
|
||||||
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.4.2_amd64.deb`
|
|
||||||
|
|
||||||
then install through dpkg:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
dpkg -i termscp_*.deb
|
|
||||||
# Or even better with gdebi
|
|
||||||
gdebi termscp_*.deb
|
|
||||||
```
|
|
||||||
|
|
||||||
### RPM package 📦
|
|
||||||
|
|
||||||
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.4.2-1.x86_64.rpm)
|
|
||||||
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.4.2-1.x86_64.rpm`
|
|
||||||
|
|
||||||
then install through rpm:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
rpm -U termscp_*.rpm
|
|
||||||
```
|
|
||||||
|
|
||||||
### AUR Package 🔼
|
|
||||||
|
|
||||||
On Arch Linux based distribution, you can install termscp using for istance [yay](https://github.com/Jguer/yay), which I recommend to install AUR packages.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yay -S termscp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Chocolatey 🍫
|
|
||||||
|
|
||||||
You can install TermSCP on Windows using [chocolatey](https://chocolatey.org/)
|
|
||||||
|
|
||||||
Start PowerShell as administrator and run
|
|
||||||
|
|
||||||
```ps
|
|
||||||
choco install termscp
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.4.2.nupkg)
|
|
||||||
|
|
||||||
and then with PowerShell started with administrator previleges, run:
|
|
||||||
|
|
||||||
```ps
|
|
||||||
choco install termscp -s .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Brew 🍻
|
|
||||||
|
|
||||||
You can install TermSCP on MacOS using [brew](https://brew.sh/)
|
|
||||||
|
|
||||||
From your terminal run
|
|
||||||
|
|
||||||
```sh
|
|
||||||
brew install veeso/termscp/termscp
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## User Manual 🎓
|
## Buy me a coffee ☕
|
||||||
|
|
||||||
[Click here](docs/man.md) to read the user manual!
|
If you like termscp and you'd love to see the project to grow, please consider a little donation 🥳
|
||||||
|
|
||||||
What you will find:
|
[](https://www.buymeacoffee.com/veeso)
|
||||||
|
|
||||||
- CLI options
|
|
||||||
- Keybindings
|
|
||||||
- Bookmarks
|
|
||||||
- Configuration
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -176,25 +87,21 @@ The developer documentation can be found on Rust Docs at <https://docs.rs/termsc
|
|||||||
|
|
||||||
## Known issues 🧻
|
## Known issues 🧻
|
||||||
|
|
||||||
- `NoSuchFileOrDirectory` on connect (WSL): I know about this issue and it's a glitch of WSL I guess. Don't worry about it, just move the termscp executable into another PATH location, such as `/usr/bin`, or install it through the appropriate package format (e.g. deb).
|
- `NoSuchFileOrDirectory` on connect (WSL1): I know about this issue and it's a glitch of WSL I guess. Don't worry about it, just move the termscp executable into another PATH location, such as `/usr/bin`, or install it through the appropriate package format (e.g. deb).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Upcoming Features 🧪
|
## Upcoming Features 🧪
|
||||||
|
|
||||||
- **Themes provider 🎨**: I'm still thinking about how I will implement this, but basically the idea is to have a configuration file where it will be possible
|
Major termscp updates will now be seasonal, so expect 4 major updates during the year.
|
||||||
to define the color schema for the entire application. I haven't planned this release yet
|
|
||||||
- **Local and remote file explorer format 🃏**: From 0.5.0 you will be able to customize the file format for both local and remote hosts.
|
|
||||||
- **Synchronized browsing of local and remote directories ⌚**: See [Issue 8](https://github.com/veeso/termscp/issues/8)
|
|
||||||
- **Group file select 🤩**: Possibility to select a group of files in explorers to operate on
|
|
||||||
|
|
||||||
No other new feature is planned at the moment. I actually think that termscp is getting mature and now I should focus upcoming updates more on bug fixing and
|
- **Keyring-rs on Linux 🔐**: Planned for the *summer update*, check for updates in [this issue](https://github.com/veeso/termscp/issues/2)
|
||||||
code/performance improvements than on new features.
|
- **Samba Support 🎉**: This will require a long time to be implemented, since I'm thinking of implementing a Rust native samba library from scratch, since I don't want to add new C-bindings. It'll maybe included in the *summer update*.
|
||||||
Anyway there are some ideas which I'd like to implement. If you want to start working on them, feel free to open a PR:
|
- **Themes provider 🎨**: I'm still thinking about how I will implement this, but basically the idea is to have a configuration file where it will be possible to define the color schema for the entire application. I haven't planned this release yet
|
||||||
|
- **Configuration profile for bookmarks 📚**: I would like to, but I still have to analyze it.
|
||||||
|
- **AWS S3 support 🪣**: There is already a library for AWS S3, but this is really on bottom of my implementation list at the moment, due to interest and I don't really have a system where to test it.
|
||||||
|
|
||||||
- Amazon S3 support
|
Along to new features, termscp developments is now focused on UI and performance improvements, so if you have any suggestion, feel free to open an issue.
|
||||||
- Samba support
|
|
||||||
- Themes provider
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -209,13 +116,13 @@ Please follow [our contributing guidelines](CONTRIBUTING.md)
|
|||||||
|
|
||||||
## Changelog ⏳
|
## Changelog ⏳
|
||||||
|
|
||||||
View TermSCP's changelog [HERE](CHANGELOG.md)
|
View termscp's changelog [HERE](CHANGELOG.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Powered by 🚀
|
## Powered by 💪
|
||||||
|
|
||||||
TermSCP is powered by these aweseome projects:
|
termscp is powered by these aweseome projects:
|
||||||
|
|
||||||
- [bytesize](https://github.com/hyunsik/bytesize)
|
- [bytesize](https://github.com/hyunsik/bytesize)
|
||||||
- [crossterm](https://github.com/crossterm-rs/crossterm)
|
- [crossterm](https://github.com/crossterm-rs/crossterm)
|
||||||
@@ -226,7 +133,9 @@ TermSCP is powered by these aweseome projects:
|
|||||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||||
- [textwrap](https://github.com/mgeisler/textwrap)
|
- [textwrap](https://github.com/mgeisler/textwrap)
|
||||||
- [tui-rs](https://github.com/fdehau/tui-rs)
|
- [tui-rs](https://github.com/fdehau/tui-rs)
|
||||||
|
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||||
- [whoami](https://github.com/libcala/whoami)
|
- [whoami](https://github.com/libcala/whoami)
|
||||||
|
- [wildmatch](https://github.com/becheran/wildmatch)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -250,14 +159,6 @@ TermSCP is powered by these aweseome projects:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Buy me a coffee ☕
|
|
||||||
|
|
||||||
If you like termscp and you'd love to see the project to grow, please consider a little donation 🥳
|
|
||||||
|
|
||||||
[](https://www.buymeacoffee.com/veeso)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License 📃
|
## License 📃
|
||||||
|
|
||||||
termscp is licensed under the MIT license.
|
termscp is licensed under the MIT license.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 472 KiB After Width: | Height: | Size: 453 KiB |
|
Before Width: | Height: | Size: 504 KiB After Width: | Height: | Size: 2.7 MiB |
@@ -1,13 +1,13 @@
|
|||||||
pkgbase = termscp
|
pkgbase = termscp
|
||||||
pkgdesc = TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal.
|
pkgdesc = termscp is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal.
|
||||||
pkgver = 0.4.2
|
pkgver = 0.5.0
|
||||||
pkgrel = 1
|
pkgrel = 1
|
||||||
url = https://github.com/veeso/termscp
|
url = https://github.com/veeso/termscp
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
license = MIT
|
license = MIT
|
||||||
provides = termscp
|
provides = termscp
|
||||||
options = strip
|
options = strip
|
||||||
source = https://github.com/veeso/termscp/releases/download/v0.4.2/termscp-0.4.2-x86_64.tar.gz
|
source = https://github.com/veeso/termscp/releases/download/v0.5.0/termscp-0.5.0-x86_64.tar.gz
|
||||||
sha256sums = c72f78a4707402f7f970a883899f4f1583fd9eca6166cb7f7616be97cabf768a
|
sha256sums = c72f78a4707402f7f970a883899f4f1583fd9eca6166cb7f7616be97cabf768a
|
||||||
|
|
||||||
pkgname = termscp
|
pkgname = termscp
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Maintainer: Christian Visintin
|
# Maintainer: Christian Visintin
|
||||||
pkgname=termscp
|
pkgname=termscp
|
||||||
pkgver=0.4.2
|
pkgver=0.5.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal."
|
pkgdesc="termscp is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal."
|
||||||
url="https://github.com/veeso/termscp"
|
url="https://github.com/veeso/termscp"
|
||||||
license=("MIT")
|
license=("MIT")
|
||||||
arch=("x86_64")
|
arch=("x86_64")
|
||||||
|
|||||||
@@ -3,18 +3,18 @@
|
|||||||
Document audience: developers
|
Document audience: developers
|
||||||
|
|
||||||
- [Developer Manual](#developer-manual)
|
- [Developer Manual](#developer-manual)
|
||||||
- [How TermSCP works](#how-termscp-works)
|
- [How termscp works](#how-termscp-works)
|
||||||
- [Activities](#activities)
|
- [Activities](#activities)
|
||||||
- [The Context](#the-context)
|
- [The Context](#the-context)
|
||||||
- [Tests fails due to receivers](#tests-fails-due-to-receivers)
|
- [Tests fails due to receivers](#tests-fails-due-to-receivers)
|
||||||
- [Implementing File Transfers](#implementing-file-transfers)
|
- [Implementing File Transfers](#implementing-file-transfers)
|
||||||
|
|
||||||
Welcome to the developer manual for TermSCP. This chapter DOESN'T contain the documentation for TermSCP modules, which can instead be found on Rust Docs at <https://docs.rs/termscp>
|
Welcome to the developer manual for termscp. This chapter DOESN'T contain the documentation for termscp modules, which can instead be found on Rust Docs at <https://docs.rs/termscp>
|
||||||
This chapter describes how TermSCP works and the guide lines to implement stuff such as file transfers and add features to the user interface.
|
This chapter describes how termscp works and the guide lines to implement stuff such as file transfers and add features to the user interface.
|
||||||
|
|
||||||
## How TermSCP works
|
## How termscp works
|
||||||
|
|
||||||
TermSCP is basically made up of 4 components:
|
termscp is basically made up of 4 components:
|
||||||
|
|
||||||
- the **filetransfer**: the filetransfer takes care of managing the remote file system; it provides function to establish a connection with the remote, operating on the remote server file system (e.g. remove files, make directories, rename files, ...), read files and write files. The FileTransfer, as we'll see later, is actually a trait, and for each protocol a FileTransfer must be implement the trait.
|
- the **filetransfer**: the filetransfer takes care of managing the remote file system; it provides function to establish a connection with the remote, operating on the remote server file system (e.g. remove files, make directories, rename files, ...), read files and write files. The FileTransfer, as we'll see later, is actually a trait, and for each protocol a FileTransfer must be implement the trait.
|
||||||
- the **host**: the host module provides functions to interact with the local host file system.
|
- the **host**: the host module provides functions to interact with the local host file system.
|
||||||
@@ -70,7 +70,7 @@ Yes. This happens quite often and is related to the fact that I'm using public S
|
|||||||
|
|
||||||
## Implementing File Transfers
|
## Implementing File Transfers
|
||||||
|
|
||||||
This chapter describes how to implement a file transfer in TermSCP. A file transfer is a module which implements the `FileTransfer` trait. The file transfer provides different modules to interact with a remote server, which in addition to the most obvious methods, used to download and upload files, provides also methods to list files, delete files, create directories etc.
|
This chapter describes how to implement a file transfer in termscp. A file transfer is a module which implements the `FileTransfer` trait. The file transfer provides different modules to interact with a remote server, which in addition to the most obvious methods, used to download and upload files, provides also methods to list files, delete files, create directories etc.
|
||||||
|
|
||||||
In the following steps I will describe how to implement a new file transfer, in this case I will be implementing the SCP file transfer (which I'm actually implementing the moment I'm writing this lines).
|
In the following steps I will describe how to implement a new file transfer, in this case I will be implementing the SCP file transfer (which I'm actually implementing the moment I'm writing this lines).
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ In the following steps I will describe how to implement a new file transfer, in
|
|||||||
```rs
|
```rs
|
||||||
/// ## FileTransferProtocol
|
/// ## FileTransferProtocol
|
||||||
///
|
///
|
||||||
/// This enum defines the different transfer protocol available in TermSCP
|
/// This enum defines the different transfer protocol available in termscp
|
||||||
#[derive(std::cmp::PartialEq, std::fmt::Debug, std::clone::Clone)]
|
#[derive(std::cmp::PartialEq, std::fmt::Debug, std::clone::Clone)]
|
||||||
pub enum FileTransferProtocol {
|
pub enum FileTransferProtocol {
|
||||||
Sftp,
|
Sftp,
|
||||||
|
|||||||
@@ -1,31 +1,17 @@
|
|||||||
# User manual 🎓
|
# User manual 🎓
|
||||||
|
|
||||||
- [User manual 🎓](#user-manual-)
|
|
||||||
- [Usage ❓](#usage-)
|
|
||||||
- [Address argument 🌎](#address-argument-)
|
|
||||||
- [How Password can be provided 🔐](#how-password-can-be-provided-)
|
|
||||||
- [Keybindings ⌨](#keybindings-)
|
|
||||||
- [Bookmarks ⭐](#bookmarks-)
|
|
||||||
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
|
|
||||||
- [Configuration ⚙️](#configuration-️)
|
|
||||||
- [SSH Key Storage 🔐](#ssh-key-storage-)
|
|
||||||
- [File Explorer Format](#file-explorer-format)
|
|
||||||
- [Text Editor ✏](#text-editor-)
|
|
||||||
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage ❓
|
## Usage ❓
|
||||||
|
|
||||||
TermSCP can be started with the following options:
|
termscp can be started with the following options:
|
||||||
|
|
||||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||||
|
|
||||||
- `-P, --password <password>` if address is provided, password will be this argument
|
- `-P, --password <password>` if address is provided, password will be this argument
|
||||||
|
- `-q, --quiet` Disable logging
|
||||||
- `-v, --version` Print version info
|
- `-v, --version` Print version info
|
||||||
- `-h, --help` Print help page
|
- `-h, --help` Print help page
|
||||||
|
|
||||||
TermSCP can be started in two different mode, if no extra arguments is provided, TermSCP will show the authentication form, where the user will be able to provide the parameters required to connect to the remote peer.
|
termscp can be started in two different mode, if no extra arguments is provided, termscp will show the authentication form, where the user will be able to provide the parameters required to connect to the remote peer.
|
||||||
|
|
||||||
Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server.
|
Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server.
|
||||||
|
|
||||||
@@ -76,7 +62,18 @@ Password can be basically provided through 3 ways when address argument is provi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Keybindings ⌨
|
## File explorer 📂
|
||||||
|
|
||||||
|
When we refer to file explorers in termscp, we refer to the panels you can see after establishing a connection with the remote.
|
||||||
|
These panels are basically 3 (yes, three actually):
|
||||||
|
|
||||||
|
- Local explorer panel: it is displayed on the left of your screen and shows the current directory entries for localhost
|
||||||
|
- Remote explorer panel: it is displayed on the right of your screen and shows the current directory entries for the remote host.
|
||||||
|
- Find results panel: depending on where you're searching for files (local/remote) it will replace the local or the explorer panel. This panel shows the entries matching the search query you performed.
|
||||||
|
|
||||||
|
In order to change panel you need to type `<LEFT>` to move the remote explorer panel and `<RIGHT>` to move back to the local explorer panel. Whenever you are in the find results panel, you need to press `<ESC>` to exit panel and go back to the previous panel.
|
||||||
|
|
||||||
|
### Keybindings ⌨
|
||||||
|
|
||||||
| Key | Command | Reminder |
|
| Key | Command | Reminder |
|
||||||
|---------------|-------------------------------------------------------|-------------|
|
|---------------|-------------------------------------------------------|-------------|
|
||||||
@@ -100,23 +97,45 @@ Password can be basically provided through 3 ways when address argument is provi
|
|||||||
| `<G>` | Go to supplied path | Go to |
|
| `<G>` | Go to supplied path | Go to |
|
||||||
| `<H>` | Show help | Help |
|
| `<H>` | Show help | Help |
|
||||||
| `<I>` | Show info about selected file or directory | Info |
|
| `<I>` | Show info about selected file or directory | Info |
|
||||||
| `<L>` | Reload current directory's content | List |
|
| `<L>` | Reload current directory's content / Clear selection | List |
|
||||||
|
| `<M>` | Select a file | Mark |
|
||||||
| `<N>` | Create new file with provided name | New |
|
| `<N>` | Create new file with provided name | New |
|
||||||
| `<O>` | Edit file; see [Text editor](#text-editor-) | Open |
|
| `<O>` | Edit file; see Text editor | Open |
|
||||||
| `<Q>` | Quit TermSCP | Quit |
|
| `<Q>` | Quit termscp | Quit |
|
||||||
| `<R>` | Rename file | Rename |
|
| `<R>` | Rename file | Rename |
|
||||||
| `<S>` | Save file as... | Save |
|
| `<S>` | Save file as... | Save |
|
||||||
| `<U>` | Go to parent directory | Upper |
|
| `<U>` | Go to parent directory | Upper |
|
||||||
| `<X>` | Execute a command | eXecute |
|
| `<X>` | Execute a command | eXecute |
|
||||||
|
| `<Y>` | Toggle synchronized browsing | sYnc |
|
||||||
| `<DEL>` | Delete file | |
|
| `<DEL>` | Delete file | |
|
||||||
|
| `<CTRL+A>` | Select all files | |
|
||||||
| `<CTRL+C>` | Abort file transfer process | |
|
| `<CTRL+C>` | Abort file transfer process | |
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Synchronized browsing ⏲️
|
||||||
|
|
||||||
|
When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels.
|
||||||
|
This means that whenever you'll change the working directory on one panel, the same action will be reproduced on the other panel. If you want to enable synchronized browsing just press `<Y>`; press twice to disable. While enabled, the synchronized browising state will be reported on the status bar on `ON`.
|
||||||
|
|
||||||
|
*Warning*: at the moment, whenever you try to access an unexisting directory, you won't be prompted to create it. This might change in a future update.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Bookmarks ⭐
|
## Bookmarks ⭐
|
||||||
|
|
||||||
In TermSCP it is possible to save favourites hosts, which can be then loaded quickly from the main layout of termscp.
|
In termscp it is possible to save favourites hosts, which can be then loaded quickly from the main layout of termscp.
|
||||||
TermSCP will also save the last 16 hosts you connected to.
|
termscp will also save the last 16 hosts you connected to.
|
||||||
This feature allows you to load all the parameters required to connect to a certain remote, simply selecting the bookmark in the tab under the authentication form.
|
This feature allows you to load all the parameters required to connect to a certain remote, simply selecting the bookmark in the tab under the authentication form.
|
||||||
|
|
||||||
Bookmarks will be saved, if possible at:
|
Bookmarks will be saved, if possible at:
|
||||||
@@ -145,7 +164,7 @@ In order to create a new bookmark, just follow these steps:
|
|||||||
|
|
||||||
whenever you want to use the previously saved connection, just press `<TAB>` to navigate to the bookmarks list and load the bookmark parameters into the form pressing `<ENTER>`.
|
whenever you want to use the previously saved connection, just press `<TAB>` to navigate to the bookmarks list and load the bookmark parameters into the form pressing `<ENTER>`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Are my passwords Safe 😈
|
### Are my passwords Safe 😈
|
||||||
|
|
||||||
@@ -161,7 +180,7 @@ Actually [keyring-rs](https://github.com/hwchen/keyring-rs), supports Linux, but
|
|||||||
|
|
||||||
## Configuration ⚙️
|
## Configuration ⚙️
|
||||||
|
|
||||||
TermSCP supports some user defined parameters, which can be defined in the configuration.
|
termscp supports some user defined parameters, which can be defined in the configuration.
|
||||||
Underhood termscp has a TOML file and some other directories where all the parameters will be saved, but don't worry, you won't touch any of these files manually, since I made possible to configure termscp from its user interface entirely.
|
Underhood termscp has a TOML file and some other directories where all the parameters will be saved, but don't worry, you won't touch any of these files manually, since I made possible to configure termscp from its user interface entirely.
|
||||||
|
|
||||||
termscp, like for bookmarks, just requires to have these paths accessible:
|
termscp, like for bookmarks, just requires to have these paths accessible:
|
||||||
@@ -196,7 +215,7 @@ You can access the SSH key storage, from configuration moving to the `SSH Keys`
|
|||||||
|
|
||||||
### File Explorer Format
|
### File Explorer Format
|
||||||
|
|
||||||
It is possible through configuration to define a custom format for the file explorer. This field, with name `File formatter syntax` will define how the file entries will be displayed in the file explorer.
|
It is possible through configuration to define a custom format for the file explorer. This is possible both for local and remote host, so you can have two different syntax in use. These fields, with name `File formatter syntax (local)` and `File formatter syntax (remote)` will define how the file entries will be displayed in the file explorer.
|
||||||
The syntax for the formatter is the following `{KEY1}... {KEY2:LENGTH}... {KEY3:LENGTH:EXTRA} {KEYn}...`.
|
The syntax for the formatter is the following `{KEY1}... {KEY2:LENGTH}... {KEY3:LENGTH:EXTRA} {KEYn}...`.
|
||||||
Each key in bracket will be replaced with the related attribute, while everything outside brackets will be left unchanged.
|
Each key in bracket will be replaced with the related attribute, while everything outside brackets will be left unchanged.
|
||||||
|
|
||||||
@@ -222,11 +241,41 @@ If left empty, the default formatter syntax will be used: `{NAME:24} {PEX} {USER
|
|||||||
|
|
||||||
## Text Editor ✏
|
## Text Editor ✏
|
||||||
|
|
||||||
TermSCP has, as you might have noticed, many features, one of these is the possibility to view and edit text file. It doesn't matter if the file is located on the local host or on the remote host, termscp provides the possibility to open a file in your favourite text editor.
|
termscp has, as you might have noticed, many features, one of these is the possibility to view and edit text file. It doesn't matter if the file is located on the local host or on the remote host, termscp provides the possibility to open a file in your favourite text editor.
|
||||||
In case the file is located on remote host, the file will be first downloaded into your temporary file directory and then, **only** if changes were made to the file, re-uploaded to the remote host. TermSCP checks if you made changes to the file verifying the last modification time of the file.
|
In case the file is located on remote host, the file will be first downloaded into your temporary file directory and then, **only** if changes were made to the file, re-uploaded to the remote host. termscp checks if you made changes to the file verifying the last modification time of the file.
|
||||||
|
|
||||||
Just a reminder: **you can edit only textual file**; binary files are not supported.
|
Just a reminder: **you can edit only textual file**; binary files are not supported.
|
||||||
|
|
||||||
### How do I configure the text editor 🦥
|
### How do I configure the text editor 🦥
|
||||||
|
|
||||||
Text editor is automatically found using this [awesome crate](https://github.com/milkey-mouse/edit), if you want to change the text editor to use, change it in termscp configuration. [Read more](#configuration-️)
|
Text editor is automatically found using this [awesome crate](https://github.com/milkey-mouse/edit), if you want to change the text editor to use, change it in termscp configuration. [Read more](#configuration-️)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging 🩺
|
||||||
|
|
||||||
|
termscp writes a log file for each session, which is written at
|
||||||
|
|
||||||
|
- `$HOME/.config/termscp/termscp.log` on Linux/BSD
|
||||||
|
- `$HOME/Library/Application Support/termscp/termscp.log` on MacOs
|
||||||
|
- `FOLDERID_RoamingAppData\termscp\termscp.log` on Windows
|
||||||
|
|
||||||
|
the log won't be rotated, but will just be truncated after each launch of termscp, so if you want to report an issue and you want to attach your log file, keep in mind to save the log file in a safe place before using termscp again.
|
||||||
|
The log file always reports in *trace* level, so it is kinda verbose.
|
||||||
|
I know you might have some questions regarding log files, so I made a kind of a Q/A:
|
||||||
|
|
||||||
|
> Is it possible to reduce verbosity?
|
||||||
|
|
||||||
|
No. The reason is quite simple: when an issue happens, you must be able to know what's causing it and the only way to do that, is to have the log file with the maximum verbosity level set.
|
||||||
|
|
||||||
|
> If trace level is set for logging, is the file going to reach a huge size?
|
||||||
|
|
||||||
|
Probably not, unless you never quit termscp, but I think that's likely to happne. A long session may produce up to 10MB of log files (I said a long session), but I think a normal session won't exceed 2MB.
|
||||||
|
|
||||||
|
> I don't want logging, can I turn it off?
|
||||||
|
|
||||||
|
Yes, you can. Just start termscp with `-q or --quiet` option. You can alias termscp to make it persistent. Remember that logging is used to diagnose issues, so since behind every open source project, there should always be this kind of mutual help, keeping log files might be your way to support the project 😉. I don't want you to feel guilty, but just to say.
|
||||||
|
|
||||||
|
> Is logging safe?
|
||||||
|
|
||||||
|
If you're concerned about security, the log file doesn't contain any plain password, so don't worry and exposes the same information the sibling file `bookmarks` reports.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# -f, -y, --force, --yes
|
# -f, -y, --force, --yes
|
||||||
# Skip the confirmation prompt during installation
|
# Skip the confirmation prompt during installation
|
||||||
|
|
||||||
TERMSCP_VERSION="0.4.2"
|
TERMSCP_VERSION="0.5.0"
|
||||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||||
DEB_URL="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
DEB_URL="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
||||||
RPM_URL="${GITHUB_URL}/termscp-${TERMSCP_VERSION}-1.x86_64.rpm"
|
RPM_URL="${GITHUB_URL}/termscp-${TERMSCP_VERSION}-1.x86_64.rpm"
|
||||||
@@ -177,7 +177,7 @@ install_on_linux() {
|
|||||||
local msg
|
local msg
|
||||||
local sudo
|
local sudo
|
||||||
local archive
|
local archive
|
||||||
if "${ARCH}" != "x86_64"; then
|
if [ "${ARCH}" != "x86_64" ]; then
|
||||||
try_with_cargo "we don't distribute packages for ${ARCH} at the moment"
|
try_with_cargo "we don't distribute packages for ${ARCH} at the moment"
|
||||||
elif has yay; then
|
elif has yay; then
|
||||||
info "Detected yay on your system"
|
info "Detected yay on your system"
|
||||||
@@ -246,7 +246,8 @@ install_on_macos() {
|
|||||||
if has brew; then
|
if has brew; then
|
||||||
if has termscp; then
|
if has termscp; then
|
||||||
info "Upgrading termscp..."
|
info "Upgrading termscp..."
|
||||||
brew update && brew upgrade termscp
|
# The OR is used since someone could have installed via cargo previously
|
||||||
|
brew update && brew upgrade termscp || brew install veeso/termscp/termscp
|
||||||
else
|
else
|
||||||
info "Installing termscp..."
|
info "Installing termscp..."
|
||||||
brew install veeso/termscp/termscp
|
brew install veeso/termscp/termscp
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ use crate::host::{HostError, Localhost};
|
|||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
use crate::ui::activities::{
|
use crate::ui::activities::{
|
||||||
auth_activity::AuthActivity, filetransfer_activity::FileTransferActivity,
|
auth::AuthActivity, filetransfer::FileTransferActivity, setup::SetupActivity, Activity,
|
||||||
setup_activity::SetupActivity, Activity, ExitReason,
|
ExitReason,
|
||||||
};
|
};
|
||||||
use crate::ui::context::{Context, FileTransferParams};
|
use crate::ui::context::{Context, FileTransferParams};
|
||||||
|
|
||||||
@@ -56,6 +56,7 @@ pub enum NextActivity {
|
|||||||
pub struct ActivityManager {
|
pub struct ActivityManager {
|
||||||
context: Option<Context>,
|
context: Option<Context>,
|
||||||
interval: Duration,
|
interval: Duration,
|
||||||
|
local_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActivityManager {
|
impl ActivityManager {
|
||||||
@@ -64,19 +65,19 @@ impl ActivityManager {
|
|||||||
/// Initializes a new Activity Manager
|
/// Initializes a new Activity Manager
|
||||||
pub fn new(local_dir: &Path, interval: Duration) -> Result<ActivityManager, HostError> {
|
pub fn new(local_dir: &Path, interval: Duration) -> Result<ActivityManager, HostError> {
|
||||||
// Prepare Context
|
// Prepare Context
|
||||||
let host: Localhost = match Localhost::new(local_dir.to_path_buf()) {
|
|
||||||
Ok(h) => h,
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
// Initialize configuration client
|
// Initialize configuration client
|
||||||
let (config_client, error): (Option<ConfigClient>, Option<String>) =
|
let (config_client, error): (Option<ConfigClient>, Option<String>) =
|
||||||
match Self::init_config_client() {
|
match Self::init_config_client() {
|
||||||
Ok(cli) => (Some(cli), None),
|
Ok(cli) => (Some(cli), None),
|
||||||
Err(err) => (None, Some(err)),
|
Err(err) => {
|
||||||
|
error!("Failed to initialize config client: {}", err);
|
||||||
|
(None, Some(err))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let ctx: Context = Context::new(host, config_client, error);
|
let ctx: Context = Context::new(config_client, error);
|
||||||
Ok(ActivityManager {
|
Ok(ActivityManager {
|
||||||
context: Some(ctx),
|
context: Some(ctx),
|
||||||
|
local_dir: local_dir.to_path_buf(),
|
||||||
interval,
|
interval,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -133,6 +134,7 @@ impl ActivityManager {
|
|||||||
/// Returns when activity terminates.
|
/// Returns when activity terminates.
|
||||||
/// Returns the next activity to run
|
/// Returns the next activity to run
|
||||||
fn run_authentication(&mut self) -> Option<NextActivity> {
|
fn run_authentication(&mut self) -> Option<NextActivity> {
|
||||||
|
info!("Starting AuthActivity...");
|
||||||
// Prepare activity
|
// Prepare activity
|
||||||
let mut activity: AuthActivity = AuthActivity::default();
|
let mut activity: AuthActivity = AuthActivity::default();
|
||||||
// Prepare result
|
// Prepare result
|
||||||
@@ -140,7 +142,10 @@ impl ActivityManager {
|
|||||||
// Get context
|
// Get context
|
||||||
let ctx: Context = match self.context.take() {
|
let ctx: Context = match self.context.take() {
|
||||||
Some(ctx) => ctx,
|
Some(ctx) => ctx,
|
||||||
None => return None,
|
None => {
|
||||||
|
error!("Failed to start AuthActivity: context is None");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Create activity
|
// Create activity
|
||||||
activity.on_create(ctx);
|
activity.on_create(ctx);
|
||||||
@@ -151,16 +156,19 @@ impl ActivityManager {
|
|||||||
if let Some(exit_reason) = activity.will_umount() {
|
if let Some(exit_reason) = activity.will_umount() {
|
||||||
match exit_reason {
|
match exit_reason {
|
||||||
ExitReason::Quit => {
|
ExitReason::Quit => {
|
||||||
|
info!("AuthActivity terminated due to 'Quit'");
|
||||||
result = None;
|
result = None;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ExitReason::EnterSetup => {
|
ExitReason::EnterSetup => {
|
||||||
// User requested activity
|
// User requested activity
|
||||||
|
info!("AuthActivity terminated due to 'EnterSetup'");
|
||||||
result = Some(NextActivity::SetupActivity);
|
result = Some(NextActivity::SetupActivity);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ExitReason::Connect => {
|
ExitReason::Connect => {
|
||||||
// User submitted, set next activity
|
// User submitted, set next activity
|
||||||
|
info!("AuthActivity terminated due to 'Connect'");
|
||||||
result = Some(NextActivity::FileTransfer);
|
result = Some(NextActivity::FileTransfer);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -172,6 +180,7 @@ impl ActivityManager {
|
|||||||
}
|
}
|
||||||
// Destroy activity
|
// Destroy activity
|
||||||
self.context = activity.on_destroy();
|
self.context = activity.on_destroy();
|
||||||
|
info!("AuthActivity destroyed");
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,19 +190,35 @@ impl ActivityManager {
|
|||||||
/// Returns when activity terminates.
|
/// Returns when activity terminates.
|
||||||
/// Returns the next activity to run
|
/// Returns the next activity to run
|
||||||
fn run_filetransfer(&mut self) -> Option<NextActivity> {
|
fn run_filetransfer(&mut self) -> Option<NextActivity> {
|
||||||
|
info!("Starting FileTransferActivity");
|
||||||
// Get context
|
// Get context
|
||||||
let ctx: Context = match self.context.take() {
|
let mut ctx: Context = match self.context.take() {
|
||||||
Some(ctx) => ctx,
|
Some(ctx) => ctx,
|
||||||
None => return None,
|
None => {
|
||||||
|
error!("Failed to start FileTransferActivity: context is None");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// If ft params is None, return None
|
// If ft params is None, return None
|
||||||
let ft_params: &FileTransferParams = match ctx.ft_params.as_ref() {
|
let ft_params: &FileTransferParams = match ctx.ft_params.as_ref() {
|
||||||
Some(ft_params) => &ft_params,
|
Some(ft_params) => &ft_params,
|
||||||
None => return None,
|
None => {
|
||||||
|
error!("Failed to start FileTransferActivity: file transfer params is None");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Prepare activity
|
// Prepare activity
|
||||||
let protocol: FileTransferProtocol = ft_params.protocol;
|
let protocol: FileTransferProtocol = ft_params.protocol;
|
||||||
let mut activity: FileTransferActivity = FileTransferActivity::new(protocol);
|
let host: Localhost = match Localhost::new(self.local_dir.clone()) {
|
||||||
|
Ok(host) => host,
|
||||||
|
Err(err) => {
|
||||||
|
// Set error in context
|
||||||
|
error!("Failed to initialize localhost: {}", err);
|
||||||
|
ctx.set_error(format!("Could not initialize localhost: {}", err));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut activity: FileTransferActivity = FileTransferActivity::new(host, protocol);
|
||||||
// Prepare result
|
// Prepare result
|
||||||
let result: Option<NextActivity>;
|
let result: Option<NextActivity>;
|
||||||
// Create activity
|
// Create activity
|
||||||
@@ -205,11 +230,13 @@ impl ActivityManager {
|
|||||||
if let Some(exit_reason) = activity.will_umount() {
|
if let Some(exit_reason) = activity.will_umount() {
|
||||||
match exit_reason {
|
match exit_reason {
|
||||||
ExitReason::Quit => {
|
ExitReason::Quit => {
|
||||||
|
info!("FileTransferActivity terminated due to 'Quit'");
|
||||||
result = None;
|
result = None;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ExitReason::Disconnect => {
|
ExitReason::Disconnect => {
|
||||||
// User disconnected, set next activity to authentication
|
// User disconnected, set next activity to authentication
|
||||||
|
info!("FileTransferActivity terminated due to 'Authentication'");
|
||||||
result = Some(NextActivity::Authentication);
|
result = Some(NextActivity::Authentication);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -235,7 +262,10 @@ impl ActivityManager {
|
|||||||
// Get context
|
// Get context
|
||||||
let ctx: Context = match self.context.take() {
|
let ctx: Context = match self.context.take() {
|
||||||
Some(ctx) => ctx,
|
Some(ctx) => ctx,
|
||||||
None => return None,
|
None => {
|
||||||
|
error!("Failed to start SetupActivity: context is None");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Create activity
|
// Create activity
|
||||||
activity.on_create(ctx);
|
activity.on_create(ctx);
|
||||||
@@ -244,6 +274,7 @@ impl ActivityManager {
|
|||||||
activity.on_draw();
|
activity.on_draw();
|
||||||
// Check if activity has terminated
|
// Check if activity has terminated
|
||||||
if let Some(ExitReason::Quit) = activity.will_umount() {
|
if let Some(ExitReason::Quit) = activity.will_umount() {
|
||||||
|
info!("SetupActivity terminated due to 'Quit'");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Sleep for ticks
|
// Sleep for ticks
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ impl std::fmt::Display for SerializerError {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bookmarks_bookmark_new() {
|
fn test_bookmarks_bookmark_new() {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ impl BookmarkSerializer {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
trace!("Serialized new bookmarks data: {}", data);
|
||||||
// Write file
|
// Write file
|
||||||
match writable.write_all(data.as_bytes()) {
|
match writable.write_all(data.as_bytes()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -72,9 +73,13 @@ impl BookmarkSerializer {
|
|||||||
err.to_string(),
|
err.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
trace!("Read bookmarks from file: {}", data);
|
||||||
// Deserialize
|
// Deserialize
|
||||||
match toml::de::from_str(data.as_str()) {
|
match toml::de::from_str(data.as_str()) {
|
||||||
Ok(hosts) => Ok(hosts),
|
Ok(bookmarks) => {
|
||||||
|
debug!("Read bookmarks from file {:?}", bookmarks);
|
||||||
|
Ok(bookmarks)
|
||||||
|
}
|
||||||
Err(err) => Err(SerializerError::new_ex(
|
Err(err) => Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::SyntaxError,
|
SerializerErrorKind::SyntaxError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -91,6 +96,7 @@ mod tests {
|
|||||||
use super::super::Bookmark;
|
use super::super::Bookmark;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::{Seek, SeekFrom};
|
use std::io::{Seek, SeekFrom};
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ pub struct UserInterfaceConfig {
|
|||||||
pub show_hidden_files: bool,
|
pub show_hidden_files: bool,
|
||||||
pub check_for_updates: Option<bool>, // @! Since 0.3.3
|
pub check_for_updates: Option<bool>, // @! Since 0.3.3
|
||||||
pub group_dirs: Option<String>,
|
pub group_dirs: Option<String>,
|
||||||
pub file_fmt: Option<String>,
|
pub file_fmt: Option<String>, // Refers to local host (for backward compatibility)
|
||||||
|
pub remote_file_fmt: Option<String>, // @! Since 0.5.0
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||||
@@ -92,6 +93,7 @@ impl Default for UserInterfaceConfig {
|
|||||||
check_for_updates: Some(true),
|
check_for_updates: Some(true),
|
||||||
group_dirs: None,
|
group_dirs: None,
|
||||||
file_fmt: None,
|
file_fmt: None,
|
||||||
|
remote_file_fmt: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,6 +163,7 @@ impl std::fmt::Display for SerializerError {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -178,6 +181,7 @@ mod tests {
|
|||||||
check_for_updates: Some(true),
|
check_for_updates: Some(true),
|
||||||
group_dirs: Some(String::from("first")),
|
group_dirs: Some(String::from("first")),
|
||||||
file_fmt: Some(String::from("{NAME}")),
|
file_fmt: Some(String::from("{NAME}")),
|
||||||
|
remote_file_fmt: Some(String::from("{USER}")),
|
||||||
};
|
};
|
||||||
let cfg: UserConfig = UserConfig {
|
let cfg: UserConfig = UserConfig {
|
||||||
user_interface: ui,
|
user_interface: ui,
|
||||||
@@ -196,6 +200,10 @@ mod tests {
|
|||||||
assert_eq!(cfg.user_interface.check_for_updates, Some(true));
|
assert_eq!(cfg.user_interface.check_for_updates, Some(true));
|
||||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
|
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
|
||||||
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
|
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
|
||||||
|
assert_eq!(
|
||||||
|
cfg.user_interface.remote_file_fmt,
|
||||||
|
Some(String::from("{USER}"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -218,6 +226,8 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
|
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
|
||||||
assert_eq!(cfg.remote.ssh_keys.len(), 0);
|
assert_eq!(cfg.remote.ssh_keys.len(), 0);
|
||||||
|
assert!(cfg.user_interface.file_fmt.is_none());
|
||||||
|
assert!(cfg.user_interface.remote_file_fmt.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ impl ConfigSerializer {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
trace!("Serialized new configuration data: {}", data);
|
||||||
// Write file
|
// Write file
|
||||||
match writable.write_all(data.as_bytes()) {
|
match writable.write_all(data.as_bytes()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -72,9 +73,13 @@ impl ConfigSerializer {
|
|||||||
err.to_string(),
|
err.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
trace!("Read configuration from file: {}", data);
|
||||||
// Deserialize
|
// Deserialize
|
||||||
match toml::de::from_str(data.as_str()) {
|
match toml::de::from_str(data.as_str()) {
|
||||||
Ok(hosts) => Ok(hosts),
|
Ok(config) => {
|
||||||
|
debug!("Read config from file {:?}", config);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
Err(err) => Err(SerializerError::new_ex(
|
Err(err) => Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::SyntaxError,
|
SerializerErrorKind::SyntaxError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -90,6 +95,7 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::io::{Seek, SeekFrom};
|
use std::io::{Seek, SeekFrom};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -114,6 +120,10 @@ mod tests {
|
|||||||
cfg.user_interface.file_fmt,
|
cfg.user_interface.file_fmt,
|
||||||
Some(String::from("{NAME} {PEX}"))
|
Some(String::from("{NAME} {PEX}"))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.user_interface.remote_file_fmt,
|
||||||
|
Some(String::from("{NAME} {USER}")),
|
||||||
|
);
|
||||||
// Verify keys
|
// Verify keys
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*cfg.remote
|
*cfg.remote
|
||||||
@@ -149,7 +159,8 @@ mod tests {
|
|||||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||||
assert_eq!(cfg.user_interface.group_dirs, None);
|
assert_eq!(cfg.user_interface.group_dirs, None);
|
||||||
assert!(cfg.user_interface.check_for_updates.is_none());
|
assert!(cfg.user_interface.check_for_updates.is_none());
|
||||||
assert_eq!(cfg.user_interface.file_fmt, None);
|
assert!(cfg.user_interface.file_fmt.is_none());
|
||||||
|
assert!(cfg.user_interface.remote_file_fmt.is_none());
|
||||||
// Verify keys
|
// Verify keys
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*cfg.remote
|
*cfg.remote
|
||||||
@@ -208,6 +219,7 @@ mod tests {
|
|||||||
check_for_updates = true
|
check_for_updates = true
|
||||||
group_dirs = "last"
|
group_dirs = "last"
|
||||||
file_fmt = "{NAME} {PEX}"
|
file_fmt = "{NAME} {PEX}"
|
||||||
|
remote_file_fmt = "{NAME} {USER}"
|
||||||
|
|
||||||
[remote.ssh_keys]
|
[remote.ssh_keys]
|
||||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ extern crate regex;
|
|||||||
|
|
||||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||||
|
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||||
use crate::utils::parser::{parse_datetime, parse_lstime};
|
use crate::utils::parser::{parse_datetime, parse_lstime};
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
@@ -105,6 +106,7 @@ impl FtpFileTransfer {
|
|||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap();
|
static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap();
|
||||||
}
|
}
|
||||||
|
debug!("Parsing LIST (UNIX) line: '{}'", line);
|
||||||
// Apply regex to result
|
// Apply regex to result
|
||||||
match LS_RE.captures(line) {
|
match LS_RE.captures(line) {
|
||||||
// String matches regex
|
// String matches regex
|
||||||
@@ -182,12 +184,12 @@ impl FtpFileTransfer {
|
|||||||
};
|
};
|
||||||
// Check if file_name is '.' or '..'
|
// Check if file_name is '.' or '..'
|
||||||
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
||||||
|
debug!("File name is {}; ignoring entry", file_name);
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
// Get symlink
|
// Get symlink
|
||||||
let symlink: Option<Box<FsEntry>> = match symlink_path {
|
let symlink: Option<Box<FsEntry>> = symlink_path.map(|p| {
|
||||||
None => None,
|
Box::new(match p.to_string_lossy().ends_with('/') {
|
||||||
Some(p) => Some(Box::new(match p.to_string_lossy().ends_with('/') {
|
|
||||||
true => {
|
true => {
|
||||||
// NOTE: is_dir becomes true
|
// NOTE: is_dir becomes true
|
||||||
is_dir = true;
|
is_dir = true;
|
||||||
@@ -226,8 +228,8 @@ impl FtpFileTransfer {
|
|||||||
group: gid,
|
group: gid,
|
||||||
unix_pex: Some(unix_pex),
|
unix_pex: Some(unix_pex),
|
||||||
}),
|
}),
|
||||||
})),
|
})
|
||||||
};
|
});
|
||||||
let mut abs_path: PathBuf = PathBuf::from(path);
|
let mut abs_path: PathBuf = PathBuf::from(path);
|
||||||
abs_path.push(file_name.as_str());
|
abs_path.push(file_name.as_str());
|
||||||
let abs_path: PathBuf = Self::resolve(abs_path.as_path());
|
let abs_path: PathBuf = Self::resolve(abs_path.as_path());
|
||||||
@@ -237,6 +239,19 @@ impl FtpFileTransfer {
|
|||||||
.extension()
|
.extension()
|
||||||
.map(|s| String::from(s.to_string_lossy()));
|
.map(|s| String::from(s.to_string_lossy()));
|
||||||
// Return
|
// Return
|
||||||
|
debug!("Follows LIST line '{}' attributes", line);
|
||||||
|
debug!("Is directory? {}", is_dir);
|
||||||
|
debug!("Is symlink? {}", is_symlink);
|
||||||
|
debug!("name: {}", file_name);
|
||||||
|
debug!("abs_path: {}", abs_path.display());
|
||||||
|
debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("symlink: {:?}", symlink);
|
||||||
|
debug!("user: {:?}", uid);
|
||||||
|
debug!("group: {:?}", gid);
|
||||||
|
debug!("unix_pex: {:?}", unix_pex);
|
||||||
|
debug!("---------------------------------------");
|
||||||
// Push to entries
|
// Push to entries
|
||||||
Ok(match is_dir {
|
Ok(match is_dir {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => FsEntry::Directory(FsDirectory {
|
||||||
@@ -288,6 +303,7 @@ impl FtpFileTransfer {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
debug!("Parsing LIST (DOS) line: '{}'", line);
|
||||||
// Apply regex to result
|
// Apply regex to result
|
||||||
match DOS_RE.captures(line) {
|
match DOS_RE.captures(line) {
|
||||||
// String matches regex
|
// String matches regex
|
||||||
@@ -325,6 +341,14 @@ impl FtpFileTransfer {
|
|||||||
.as_path()
|
.as_path()
|
||||||
.extension()
|
.extension()
|
||||||
.map(|s| String::from(s.to_string_lossy()));
|
.map(|s| String::from(s.to_string_lossy()));
|
||||||
|
debug!("Follows LIST line '{}' attributes", line);
|
||||||
|
debug!("Is directory? {}", is_dir);
|
||||||
|
debug!("name: {}", file_name);
|
||||||
|
debug!("abs_path: {}", abs_path.display());
|
||||||
|
debug!("last_change_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("last_access_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("creation_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("---------------------------------------");
|
||||||
// Return entry
|
// Return entry
|
||||||
Ok(match is_dir {
|
Ok(match is_dir {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => FsEntry::Directory(FsDirectory {
|
||||||
@@ -383,17 +407,20 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) -> Result<Option<String>, FileTransferError> {
|
) -> Result<Option<String>, FileTransferError> {
|
||||||
// Get stream
|
// Get stream
|
||||||
|
info!("Connecting to {}:{}", address, port);
|
||||||
let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) {
|
let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) {
|
||||||
Ok(stream) => stream,
|
Ok(stream) => stream,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Failed to connect: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// If SSL, open secure session
|
// If SSL, open secure session
|
||||||
if self.ftps {
|
if self.ftps {
|
||||||
|
info!("Setting up TLS stream...");
|
||||||
let ctx = match TlsConnector::builder()
|
let ctx = match TlsConnector::builder()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
.danger_accept_invalid_hostnames(true)
|
.danger_accept_invalid_hostnames(true)
|
||||||
@@ -401,19 +428,21 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
{
|
{
|
||||||
Ok(tls) => tls,
|
Ok(tls) => tls,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Failed to setup TLS stream: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::SslError,
|
FileTransferErrorType::SslError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
stream = match stream.into_secure(ctx, address.as_str()) {
|
stream = match stream.into_secure(ctx, address.as_str()) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Failed to setup TLS stream: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::SslError,
|
FileTransferErrorType::SslError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -426,14 +455,22 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
Some(pwd) => pwd,
|
Some(pwd) => pwd,
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
info!(
|
||||||
|
"Signin in with username: {}, password: {}",
|
||||||
|
username,
|
||||||
|
shadow_password(password.as_str())
|
||||||
|
);
|
||||||
if let Err(err) = stream.login(username.as_str(), password.as_str()) {
|
if let Err(err) = stream.login(username.as_str(), password.as_str()) {
|
||||||
|
error!("Login failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::AuthenticationFailed,
|
FileTransferErrorType::AuthenticationFailed,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
debug!("Setting transfer type to Binary");
|
||||||
// Initialize file type
|
// Initialize file type
|
||||||
if let Err(err) = stream.transfer_type(FileType::Binary) {
|
if let Err(err) = stream.transfer_type(FileType::Binary) {
|
||||||
|
error!("Failed to set transfer type to binary: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ProtocolError,
|
FileTransferErrorType::ProtocolError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -441,6 +478,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
}
|
}
|
||||||
// Set stream
|
// Set stream
|
||||||
self.stream = Some(stream);
|
self.stream = Some(stream);
|
||||||
|
info!("Connection successfully established");
|
||||||
// Return OK
|
// Return OK
|
||||||
Ok(self.stream.as_ref().unwrap().get_welcome_msg())
|
Ok(self.stream.as_ref().unwrap().get_welcome_msg())
|
||||||
}
|
}
|
||||||
@@ -450,6 +488,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Disconnect from the remote server
|
/// Disconnect from the remote server
|
||||||
|
|
||||||
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
||||||
|
info!("Disconnecting from FTP server...");
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.quit() {
|
Some(stream) => match stream.quit() {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -476,6 +515,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Print working directory
|
/// Print working directory
|
||||||
|
|
||||||
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
||||||
|
info!("PWD");
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.pwd() {
|
Some(stream) => match stream.pwd() {
|
||||||
Ok(path) => Ok(PathBuf::from(path.as_str())),
|
Ok(path) => Ok(PathBuf::from(path.as_str())),
|
||||||
@@ -496,6 +536,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
|
|
||||||
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError> {
|
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError> {
|
||||||
let dir: PathBuf = Self::resolve(dir);
|
let dir: PathBuf = Self::resolve(dir);
|
||||||
|
info!("Changing directory to {}", dir.display());
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) {
|
Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) {
|
||||||
Ok(_) => Ok(dir),
|
Ok(_) => Ok(dir),
|
||||||
@@ -515,6 +556,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Copy file to destination
|
/// Copy file to destination
|
||||||
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> {
|
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> {
|
||||||
// FTP doesn't support file copy
|
// FTP doesn't support file copy
|
||||||
|
debug!("COPY issues (will fail, since unsupported)");
|
||||||
Err(FileTransferError::new(
|
Err(FileTransferError::new(
|
||||||
FileTransferErrorType::UnsupportedFeature,
|
FileTransferErrorType::UnsupportedFeature,
|
||||||
))
|
))
|
||||||
@@ -526,9 +568,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
|
|
||||||
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
|
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
|
||||||
let dir: PathBuf = Self::resolve(path);
|
let dir: PathBuf = Self::resolve(path);
|
||||||
|
info!("LIST dir {}", dir.display());
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) {
|
Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
|
debug!("Got {} lines in LIST result", entries.len());
|
||||||
// Prepare result
|
// Prepare result
|
||||||
let mut result: Vec<FsEntry> = Vec::with_capacity(entries.len());
|
let mut result: Vec<FsEntry> = Vec::with_capacity(entries.len());
|
||||||
// Iterate over entries
|
// Iterate over entries
|
||||||
@@ -537,6 +581,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
result.push(file);
|
result.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
debug!(
|
||||||
|
"{} out of {} were valid entries",
|
||||||
|
result.len(),
|
||||||
|
entries.len()
|
||||||
|
);
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -555,6 +604,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Make directory
|
/// Make directory
|
||||||
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
|
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
|
||||||
let dir: PathBuf = Self::resolve(dir);
|
let dir: PathBuf = Self::resolve(dir);
|
||||||
|
info!("MKDIR {}", dir.display());
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) {
|
Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -578,9 +628,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
FileTransferErrorType::UninitializedSession,
|
FileTransferErrorType::UninitializedSession,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
info!("Removing entry {}", fsentry.get_abs_path().display());
|
||||||
match fsentry {
|
match fsentry {
|
||||||
// Match fs entry...
|
// Match fs entry...
|
||||||
FsEntry::File(file) => {
|
FsEntry::File(file) => {
|
||||||
|
debug!("entry is a file; removing file");
|
||||||
// Remove file directly
|
// Remove file directly
|
||||||
match self.stream.as_mut().unwrap().rm(file.name.as_ref()) {
|
match self.stream.as_mut().unwrap().rm(file.name.as_ref()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -592,9 +644,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
}
|
}
|
||||||
FsEntry::Directory(dir) => {
|
FsEntry::Directory(dir) => {
|
||||||
// Get directory files
|
// Get directory files
|
||||||
|
debug!("Entry is a directory; iterating directory entries");
|
||||||
match self.list_dir(dir.abs_path.as_path()) {
|
match self.list_dir(dir.abs_path.as_path()) {
|
||||||
Ok(files) => {
|
Ok(files) => {
|
||||||
// Remove recursively files
|
// Remove recursively files
|
||||||
|
debug!("Removing {} entries from directory...", files.len());
|
||||||
for file in files.iter() {
|
for file in files.iter() {
|
||||||
if let Err(err) = self.remove(&file) {
|
if let Err(err) = self.remove(&file) {
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
@@ -604,6 +658,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Once all files in directory have been deleted, remove directory
|
// Once all files in directory have been deleted, remove directory
|
||||||
|
debug!("Finally removing directory {}", dir.name);
|
||||||
match self.stream.as_mut().unwrap().rmdir(dir.name.as_str()) {
|
match self.stream.as_mut().unwrap().rmdir(dir.name.as_str()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -626,6 +681,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Rename file or a directory
|
/// Rename file or a directory
|
||||||
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
|
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
|
||||||
let dst: PathBuf = Self::resolve(dst);
|
let dst: PathBuf = Self::resolve(dst);
|
||||||
|
info!(
|
||||||
|
"Renaming {} to {}",
|
||||||
|
file.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => {
|
Some(stream) => {
|
||||||
// Get name
|
// Get name
|
||||||
@@ -692,6 +752,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
file_name: &Path,
|
file_name: &Path,
|
||||||
) -> Result<Box<dyn Write>, FileTransferError> {
|
) -> Result<Box<dyn Write>, FileTransferError> {
|
||||||
let file_name: PathBuf = Self::resolve(file_name);
|
let file_name: PathBuf = Self::resolve(file_name);
|
||||||
|
info!("Sending file {}", file_name.display());
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.put_with_stream(&file_name.as_path().to_string_lossy()) {
|
Some(stream) => match stream.put_with_stream(&file_name.as_path().to_string_lossy()) {
|
||||||
Ok(writer) => Ok(Box::new(writer)), // NOTE: don't use BufWriter here, since already returned by the library
|
Ok(writer) => Ok(Box::new(writer)), // NOTE: don't use BufWriter here, since already returned by the library
|
||||||
@@ -711,6 +772,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Receive file from remote with provided name
|
/// Receive file from remote with provided name
|
||||||
/// Returns file and its size
|
/// Returns file and its size
|
||||||
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
|
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
|
||||||
|
info!("Receiving file {}", file.abs_path.display());
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.get(&file.abs_path.as_path().to_string_lossy()) {
|
Some(stream) => match stream.get(&file.abs_path.as_path().to_string_lossy()) {
|
||||||
Ok(reader) => Ok(Box::new(reader)), // NOTE: don't use BufReader here, since already returned by the library
|
Ok(reader) => Ok(Box::new(reader)), // NOTE: don't use BufReader here, since already returned by the library
|
||||||
@@ -733,6 +795,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// This is necessary for some protocols such as FTP.
|
/// This is necessary for some protocols such as FTP.
|
||||||
/// You must call this method each time you want to finalize the write of the remote file.
|
/// You must call this method each time you want to finalize the write of the remote file.
|
||||||
fn on_sent(&mut self, writable: Box<dyn Write>) -> Result<(), FileTransferError> {
|
fn on_sent(&mut self, writable: Box<dyn Write>) -> Result<(), FileTransferError> {
|
||||||
|
info!("Finalizing put stream");
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.finalize_put_stream(writable) {
|
Some(stream) => match stream.finalize_put_stream(writable) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -755,6 +818,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// This mighe be necessary for some protocols.
|
/// This mighe be necessary for some protocols.
|
||||||
/// You must call this method each time you want to finalize the read of the remote file.
|
/// You must call this method each time you want to finalize the read of the remote file.
|
||||||
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError> {
|
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError> {
|
||||||
|
info!("Finalizing get");
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.finalize_get(readable) {
|
Some(stream) => match stream.finalize_get(readable) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -775,6 +839,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::utils::fmt::fmt_time;
|
use crate::utils::fmt::fmt_time;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub mod sftp_transfer;
|
|||||||
|
|
||||||
/// ## FileTransferProtocol
|
/// ## FileTransferProtocol
|
||||||
///
|
///
|
||||||
/// This enum defines the different transfer protocol available in TermSCP
|
/// This enum defines the different transfer protocol available in termscp
|
||||||
|
|
||||||
#[derive(PartialEq, std::fmt::Debug, std::clone::Clone, Copy)]
|
#[derive(PartialEq, std::fmt::Debug, std::clone::Clone, Copy)]
|
||||||
pub enum FileTransferProtocol {
|
pub enum FileTransferProtocol {
|
||||||
@@ -59,11 +59,19 @@ pub struct FileTransferError {
|
|||||||
msg: Option<String>,
|
msg: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FileTransferError {
|
||||||
|
/// ### kind
|
||||||
|
///
|
||||||
|
/// Returns the error kind
|
||||||
|
pub fn kind(&self) -> FileTransferErrorType {
|
||||||
|
self.code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// ## FileTransferErrorType
|
/// ## FileTransferErrorType
|
||||||
///
|
///
|
||||||
/// FileTransferErrorType defines the possible errors available for a file transfer
|
/// FileTransferErrorType defines the possible errors available for a file transfer
|
||||||
#[allow(dead_code)]
|
#[derive(Error, Debug, Clone, Copy, PartialEq)]
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum FileTransferErrorType {
|
pub enum FileTransferErrorType {
|
||||||
#[error("Authentication failed")]
|
#[error("Authentication failed")]
|
||||||
AuthenticationFailed,
|
AuthenticationFailed,
|
||||||
@@ -77,8 +85,6 @@ pub enum FileTransferErrorType {
|
|||||||
DirStatFailed,
|
DirStatFailed,
|
||||||
#[error("Failed to create file")]
|
#[error("Failed to create file")]
|
||||||
FileCreateDenied,
|
FileCreateDenied,
|
||||||
#[error("IO error: {0}")]
|
|
||||||
IoErr(std::io::Error),
|
|
||||||
#[error("No such file or directory")]
|
#[error("No such file or directory")]
|
||||||
NoSuchFileOrDirectory,
|
NoSuchFileOrDirectory,
|
||||||
#[error("Not enough permissions")]
|
#[error("Not enough permissions")]
|
||||||
@@ -313,14 +319,14 @@ impl std::string::ToString for FileTransferProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for FileTransferProtocol {
|
impl std::str::FromStr for FileTransferProtocol {
|
||||||
type Err = ();
|
type Err = String;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_ascii_uppercase().as_str() {
|
match s.to_ascii_uppercase().as_str() {
|
||||||
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
|
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
|
||||||
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
|
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
|
||||||
"SCP" => Ok(FileTransferProtocol::Scp),
|
"SCP" => Ok(FileTransferProtocol::Scp),
|
||||||
"SFTP" => Ok(FileTransferProtocol::Sftp),
|
"SFTP" => Ok(FileTransferProtocol::Sftp),
|
||||||
_ => Err(()),
|
_ => Err(s.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,6 +338,7 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
|
||||||
@@ -396,13 +403,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_filetransfer_mod_error() {
|
fn test_filetransfer_mod_error() {
|
||||||
let err: FileTransferError = FileTransferError::new_ex(
|
let err: FileTransferError = FileTransferError::new_ex(
|
||||||
FileTransferErrorType::IoErr(std::io::Error::from(std::io::ErrorKind::AddrInUse)),
|
FileTransferErrorType::NoSuchFileOrDirectory,
|
||||||
String::from("non va una mazza"),
|
String::from("non va una mazza"),
|
||||||
);
|
);
|
||||||
assert_eq!(*err.msg.as_ref().unwrap(), String::from("non va una mazza"));
|
assert_eq!(*err.msg.as_ref().unwrap(), String::from("non va una mazza"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", err),
|
format!("{}", err),
|
||||||
String::from("IO error: address in use (non va una mazza)")
|
String::from("No such file or directory (non va una mazza)")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!(
|
format!(
|
||||||
@@ -481,5 +488,7 @@ mod tests {
|
|||||||
),
|
),
|
||||||
String::from("Unsupported feature")
|
String::from("Unsupported feature")
|
||||||
);
|
);
|
||||||
|
let err = FileTransferError::new(FileTransferErrorType::UnsupportedFeature);
|
||||||
|
assert_eq!(err.kind(), FileTransferErrorType::UnsupportedFeature);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ extern crate ssh2;
|
|||||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||||
use crate::system::sshkey_storage::SshKeyStorage;
|
use crate::system::sshkey_storage::SshKeyStorage;
|
||||||
|
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||||
use crate::utils::parser::parse_lstime;
|
use crate::utils::parser::parse_lstime;
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
@@ -90,6 +91,7 @@ impl ScpFileTransfer {
|
|||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap();
|
static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap();
|
||||||
}
|
}
|
||||||
|
debug!("Parsing LS line: '{}'", line);
|
||||||
// Apply regex to result
|
// Apply regex to result
|
||||||
match LS_RE.captures(line) {
|
match LS_RE.captures(line) {
|
||||||
// String matches regex
|
// String matches regex
|
||||||
@@ -167,6 +169,7 @@ impl ScpFileTransfer {
|
|||||||
};
|
};
|
||||||
// Check if file_name is '.' or '..'
|
// Check if file_name is '.' or '..'
|
||||||
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
||||||
|
debug!("File name is {}; ignoring entry", file_name);
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
// Get symlink; PATH mustn't be equal to filename
|
// Get symlink; PATH mustn't be equal to filename
|
||||||
@@ -200,6 +203,19 @@ impl ScpFileTransfer {
|
|||||||
.extension()
|
.extension()
|
||||||
.map(|s| String::from(s.to_string_lossy()));
|
.map(|s| String::from(s.to_string_lossy()));
|
||||||
// Return
|
// Return
|
||||||
|
debug!("Follows LS line '{}' attributes", line);
|
||||||
|
debug!("Is directory? {}", is_dir);
|
||||||
|
debug!("Is symlink? {}", is_symlink);
|
||||||
|
debug!("name: {}", file_name);
|
||||||
|
debug!("abs_path: {}", abs_path.display());
|
||||||
|
debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("symlink: {:?}", symlink);
|
||||||
|
debug!("user: {:?}", uid);
|
||||||
|
debug!("group: {:?}", gid);
|
||||||
|
debug!("unix_pex: {:?}", unix_pex);
|
||||||
|
debug!("---------------------------------------");
|
||||||
// Push to entries
|
// Push to entries
|
||||||
Ok(match is_dir {
|
Ok(match is_dir {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => FsEntry::Directory(FsDirectory {
|
||||||
@@ -262,6 +278,7 @@ impl ScpFileTransfer {
|
|||||||
fn perform_shell_cmd(&mut self, cmd: &str) -> Result<String, FileTransferError> {
|
fn perform_shell_cmd(&mut self, cmd: &str) -> Result<String, FileTransferError> {
|
||||||
match self.session.as_mut() {
|
match self.session.as_mut() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
|
debug!("Running command: {}", cmd);
|
||||||
// Create channel
|
// Create channel
|
||||||
let mut channel: Channel = match session.channel_session() {
|
let mut channel: Channel = match session.channel_session() {
|
||||||
Ok(ch) => ch,
|
Ok(ch) => ch,
|
||||||
@@ -285,6 +302,7 @@ impl ScpFileTransfer {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Wait close
|
// Wait close
|
||||||
let _ = channel.wait_close();
|
let _ = channel.wait_close();
|
||||||
|
debug!("Command output: {}", output);
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -312,6 +330,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) -> Result<Option<String>, FileTransferError> {
|
) -> Result<Option<String>, FileTransferError> {
|
||||||
// Setup tcp stream
|
// Setup tcp stream
|
||||||
|
info!("Connecting to {}:{}", address, port);
|
||||||
let socket_addresses: Vec<SocketAddr> =
|
let socket_addresses: Vec<SocketAddr> =
|
||||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||||
Ok(s) => s.collect(),
|
Ok(s) => s.collect(),
|
||||||
@@ -325,8 +344,10 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
let mut tcp: Option<TcpStream> = None;
|
let mut tcp: Option<TcpStream> = None;
|
||||||
// Try addresses
|
// Try addresses
|
||||||
for socket_addr in socket_addresses.iter() {
|
for socket_addr in socket_addresses.iter() {
|
||||||
|
debug!("Trying socket address {}", socket_addr);
|
||||||
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
|
debug!("{} succeded", socket_addr);
|
||||||
tcp = Some(stream);
|
tcp = Some(stream);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -337,26 +358,30 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
let tcp: TcpStream = match tcp {
|
let tcp: TcpStream = match tcp {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
|
error!("No suitable socket address found; connection timeout");
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
String::from("Connection timeout"),
|
String::from("Connection timeout"),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Create session
|
// Create session
|
||||||
let mut session: Session = match Session::new() {
|
let mut session: Session = match Session::new() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not create session: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Set TCP stream
|
// Set TCP stream
|
||||||
session.set_tcp_stream(tcp);
|
session.set_tcp_stream(tcp);
|
||||||
// Open connection
|
// Open connection
|
||||||
|
debug!("Initializing handshake");
|
||||||
if let Err(err) = session.handshake() {
|
if let Err(err) = session.handshake() {
|
||||||
|
error!("Handshake failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -372,6 +397,11 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
.resolve(address.as_str(), username.as_str())
|
.resolve(address.as_str(), username.as_str())
|
||||||
{
|
{
|
||||||
Some(rsa_key) => {
|
Some(rsa_key) => {
|
||||||
|
debug!(
|
||||||
|
"Authenticating with user {} and RSA key {}",
|
||||||
|
username,
|
||||||
|
rsa_key.display()
|
||||||
|
);
|
||||||
// Authenticate with RSA key
|
// Authenticate with RSA key
|
||||||
if let Err(err) = session.userauth_pubkey_file(
|
if let Err(err) = session.userauth_pubkey_file(
|
||||||
username.as_str(),
|
username.as_str(),
|
||||||
@@ -379,6 +409,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
rsa_key.as_path(),
|
rsa_key.as_path(),
|
||||||
password.as_deref(),
|
password.as_deref(),
|
||||||
) {
|
) {
|
||||||
|
error!("Authentication failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::AuthenticationFailed,
|
FileTransferErrorType::AuthenticationFailed,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -387,10 +418,16 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Proceeed with username/password authentication
|
// Proceeed with username/password authentication
|
||||||
|
debug!(
|
||||||
|
"Authenticating with username {} and password {}",
|
||||||
|
username,
|
||||||
|
shadow_password(password.as_deref().unwrap_or(""))
|
||||||
|
);
|
||||||
if let Err(err) = session.userauth_password(
|
if let Err(err) = session.userauth_password(
|
||||||
username.as_str(),
|
username.as_str(),
|
||||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||||
) {
|
) {
|
||||||
|
error!("Authentication failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::AuthenticationFailed,
|
FileTransferErrorType::AuthenticationFailed,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -400,13 +437,22 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
}
|
}
|
||||||
// Get banner
|
// Get banner
|
||||||
let banner: Option<String> = session.banner().map(String::from);
|
let banner: Option<String> = session.banner().map(String::from);
|
||||||
|
debug!(
|
||||||
|
"Connection established: {}",
|
||||||
|
banner.as_deref().unwrap_or("")
|
||||||
|
);
|
||||||
// Set session
|
// Set session
|
||||||
self.session = Some(session);
|
self.session = Some(session);
|
||||||
// Get working directory
|
// Get working directory
|
||||||
|
debug!("Getting working directory...");
|
||||||
match self.perform_shell_cmd("pwd") {
|
match self.perform_shell_cmd("pwd") {
|
||||||
Ok(output) => self.wrkdir = PathBuf::from(output.as_str().trim()),
|
Ok(output) => self.wrkdir = PathBuf::from(output.as_str().trim()),
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
}
|
}
|
||||||
|
info!(
|
||||||
|
"Connection established; working directory: {}",
|
||||||
|
self.wrkdir.display()
|
||||||
|
);
|
||||||
Ok(banner)
|
Ok(banner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +460,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
///
|
///
|
||||||
/// Disconnect from the remote server
|
/// Disconnect from the remote server
|
||||||
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
||||||
|
info!("Disconnecting from remote...");
|
||||||
match self.session.as_ref() {
|
match self.session.as_ref() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
// Disconnect (greet server with 'Mandi' as they do in Friuli)
|
// Disconnect (greet server with 'Mandi' as they do in Friuli)
|
||||||
@@ -447,6 +494,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
/// Print working directory
|
/// Print working directory
|
||||||
|
|
||||||
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
||||||
|
info!("PWD: {}", self.wrkdir.display());
|
||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => Ok(self.wrkdir.clone()),
|
true => Ok(self.wrkdir.clone()),
|
||||||
false => Err(FileTransferError::new(
|
false => Err(FileTransferError::new(
|
||||||
@@ -471,6 +519,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
Self::resolve(p.as_path())
|
Self::resolve(p.as_path())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
info!("Changing working directory to {}", remote_path.display());
|
||||||
// Change directory
|
// Change directory
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
p.as_path(),
|
p.as_path(),
|
||||||
@@ -484,6 +533,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
true => {
|
true => {
|
||||||
// Set working directory
|
// Set working directory
|
||||||
self.wrkdir = PathBuf::from(&output.as_str()[1..].trim());
|
self.wrkdir = PathBuf::from(&output.as_str()[1..].trim());
|
||||||
|
info!("Changed working directory to {}", self.wrkdir.display());
|
||||||
Ok(self.wrkdir.clone())
|
Ok(self.wrkdir.clone())
|
||||||
}
|
}
|
||||||
false => Err(FileTransferError::new_ex(
|
false => Err(FileTransferError::new_ex(
|
||||||
@@ -512,6 +562,11 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
let dst: PathBuf = Self::resolve(dst);
|
let dst: PathBuf = Self::resolve(dst);
|
||||||
|
info!(
|
||||||
|
"Copying {} to {}",
|
||||||
|
src.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
// Run `cp -rf`
|
// Run `cp -rf`
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
@@ -555,6 +610,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
// Send ls -l to path
|
// Send ls -l to path
|
||||||
|
info!("Getting file entries in {}", path.display());
|
||||||
let path: PathBuf = Self::resolve(path);
|
let path: PathBuf = Self::resolve(path);
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
@@ -572,6 +628,11 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
info!(
|
||||||
|
"Found {} out of {} valid file entries",
|
||||||
|
entries.len(),
|
||||||
|
lines.len()
|
||||||
|
);
|
||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -594,6 +655,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
let dir: PathBuf = Self::resolve(dir);
|
let dir: PathBuf = Self::resolve(dir);
|
||||||
|
info!("Making directory {}", dir.display());
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
// Mkdir dir && echo 0
|
// Mkdir dir && echo 0
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
@@ -632,6 +694,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
true => {
|
true => {
|
||||||
// Get path
|
// Get path
|
||||||
let path: PathBuf = file.get_abs_path();
|
let path: PathBuf = file.get_abs_path();
|
||||||
|
info!("Removing file {}", path.display());
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
p.as_path(),
|
p.as_path(),
|
||||||
@@ -669,6 +732,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
// Get path
|
// Get path
|
||||||
let dst: PathBuf = Self::resolve(dst);
|
let dst: PathBuf = Self::resolve(dst);
|
||||||
let path: PathBuf = file.get_abs_path();
|
let path: PathBuf = file.get_abs_path();
|
||||||
|
info!("Renaming {} to {}", path.display(), dst.display());
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
p.as_path(),
|
p.as_path(),
|
||||||
@@ -717,6 +781,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
|
info!("Stat {}", path.display());
|
||||||
// make command; Directories require `-d` option
|
// make command; Directories require `-d` option
|
||||||
let cmd: String = match path.to_string_lossy().ends_with('/') {
|
let cmd: String = match path.to_string_lossy().ends_with('/') {
|
||||||
true => format!("ls -ld \"{}\"", path.display()),
|
true => format!("ls -ld \"{}\"", path.display()),
|
||||||
@@ -729,7 +794,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
Some(p) => PathBuf::from(p),
|
Some(p) => PathBuf::from(p),
|
||||||
None => {
|
None => {
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::UnsupportedFeature,
|
FileTransferErrorType::DirStatFailed,
|
||||||
String::from("Path has no parent"),
|
String::from("Path has no parent"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -760,6 +825,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
|
info!("Executing command {}", cmd);
|
||||||
match self.perform_shell_cmd_with_path(p.as_path(), cmd) {
|
match self.perform_shell_cmd_with_path(p.as_path(), cmd) {
|
||||||
Ok(output) => Ok(output),
|
Ok(output) => Ok(output),
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -788,7 +854,13 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.session.as_ref() {
|
match self.session.as_ref() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
let file_name: PathBuf = Self::resolve(file_name);
|
let file_name: PathBuf = Self::resolve(file_name);
|
||||||
|
info!(
|
||||||
|
"Sending file {} to {}",
|
||||||
|
local.abs_path.display(),
|
||||||
|
file_name.display()
|
||||||
|
);
|
||||||
// Set blocking to true
|
// Set blocking to true
|
||||||
|
debug!("blocking channel...");
|
||||||
session.set_blocking(true);
|
session.set_blocking(true);
|
||||||
// Calculate file mode
|
// Calculate file mode
|
||||||
let mode: i32 = match local.unix_pex {
|
let mode: i32 = match local.unix_pex {
|
||||||
@@ -818,6 +890,10 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
Ok(metadata) => metadata.len(),
|
Ok(metadata) => metadata.len(),
|
||||||
Err(_) => local.size as u64, // NOTE: fallback to fsentry size
|
Err(_) => local.size as u64, // NOTE: fallback to fsentry size
|
||||||
};
|
};
|
||||||
|
debug!(
|
||||||
|
"File mode {:?}; mtime: {}, atime: {}; file size: {}",
|
||||||
|
mode, times.0, times.1, file_size
|
||||||
|
);
|
||||||
// Send file
|
// Send file
|
||||||
match session.scp_send(file_name.as_path(), mode, file_size, Some(times)) {
|
match session.scp_send(file_name.as_path(), mode, file_size, Some(times)) {
|
||||||
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))),
|
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))),
|
||||||
@@ -840,7 +916,9 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
|
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
|
||||||
match self.session.as_ref() {
|
match self.session.as_ref() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
|
info!("Receiving file {}", file.abs_path.display());
|
||||||
// Set blocking to true
|
// Set blocking to true
|
||||||
|
debug!("Set blocking...");
|
||||||
session.set_blocking(true);
|
session.set_blocking(true);
|
||||||
match session.scp_recv(file.abs_path.as_path()) {
|
match session.scp_recv(file.abs_path.as_path()) {
|
||||||
Ok(reader) => Ok(Box::new(BufReader::with_capacity(65536, reader.0))),
|
Ok(reader) => Ok(Box::new(BufReader::with_capacity(65536, reader.0))),
|
||||||
@@ -885,6 +963,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_filetransfer_scp_new() {
|
fn test_filetransfer_scp_new() {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ extern crate ssh2;
|
|||||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||||
use crate::system::sshkey_storage::SshKeyStorage;
|
use crate::system::sshkey_storage::SshKeyStorage;
|
||||||
|
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
use ssh2::{Channel, FileStat, OpenFlags, OpenType, Session, Sftp};
|
use ssh2::{Channel, FileStat, OpenFlags, OpenType, Session, Sftp};
|
||||||
@@ -159,6 +160,19 @@ impl SftpFileTransfer {
|
|||||||
}
|
}
|
||||||
false => None,
|
false => None,
|
||||||
};
|
};
|
||||||
|
debug!("Follows {} attributes", path.display());
|
||||||
|
debug!("Is directory? {}", metadata.is_dir());
|
||||||
|
debug!("Is symlink? {}", is_symlink);
|
||||||
|
debug!("name: {}", file_name);
|
||||||
|
debug!("abs_path: {}", path.display());
|
||||||
|
debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||||
|
debug!("symlink: {:?}", symlink);
|
||||||
|
debug!("user: {:?}", uid);
|
||||||
|
debug!("group: {:?}", gid);
|
||||||
|
debug!("unix_pex: {:?}", pex);
|
||||||
|
debug!("---------------------------------------");
|
||||||
// Is a directory?
|
// Is a directory?
|
||||||
match metadata.is_dir() {
|
match metadata.is_dir() {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => FsEntry::Directory(FsDirectory {
|
||||||
@@ -205,6 +219,7 @@ impl SftpFileTransfer {
|
|||||||
match self.session.as_mut() {
|
match self.session.as_mut() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
// Create channel
|
// Create channel
|
||||||
|
debug!("Running command: {}", cmd);
|
||||||
let mut channel: Channel = match session.channel_session() {
|
let mut channel: Channel = match session.channel_session() {
|
||||||
Ok(ch) => ch,
|
Ok(ch) => ch,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -227,6 +242,7 @@ impl SftpFileTransfer {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Wait close
|
// Wait close
|
||||||
let _ = channel.wait_close();
|
let _ = channel.wait_close();
|
||||||
|
debug!("Command output: {}", output);
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -254,6 +270,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) -> Result<Option<String>, FileTransferError> {
|
) -> Result<Option<String>, FileTransferError> {
|
||||||
// Setup tcp stream
|
// Setup tcp stream
|
||||||
|
info!("Connecting to {}:{}", address, port);
|
||||||
let socket_addresses: Vec<SocketAddr> =
|
let socket_addresses: Vec<SocketAddr> =
|
||||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||||
Ok(s) => s.collect(),
|
Ok(s) => s.collect(),
|
||||||
@@ -267,6 +284,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
let mut tcp: Option<TcpStream> = None;
|
let mut tcp: Option<TcpStream> = None;
|
||||||
// Try addresses
|
// Try addresses
|
||||||
for socket_addr in socket_addresses.iter() {
|
for socket_addr in socket_addresses.iter() {
|
||||||
|
debug!("Trying socket address {}", socket_addr);
|
||||||
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
tcp = Some(stream);
|
tcp = Some(stream);
|
||||||
@@ -279,26 +297,30 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
let tcp: TcpStream = match tcp {
|
let tcp: TcpStream = match tcp {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
|
error!("No suitable socket address found; connection timeout");
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
String::from("Connection timeout"),
|
String::from("Connection timeout"),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Create session
|
// Create session
|
||||||
let mut session: Session = match Session::new() {
|
let mut session: Session = match Session::new() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not create session: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Set TCP stream
|
// Set TCP stream
|
||||||
session.set_tcp_stream(tcp);
|
session.set_tcp_stream(tcp);
|
||||||
// Open connection
|
// Open connection
|
||||||
|
debug!("Initializing handshake");
|
||||||
if let Err(err) = session.handshake() {
|
if let Err(err) = session.handshake() {
|
||||||
|
error!("Handshake failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -314,6 +336,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
.resolve(address.as_str(), username.as_str())
|
.resolve(address.as_str(), username.as_str())
|
||||||
{
|
{
|
||||||
Some(rsa_key) => {
|
Some(rsa_key) => {
|
||||||
|
debug!(
|
||||||
|
"Authenticating with user {} and RSA key {}",
|
||||||
|
username,
|
||||||
|
rsa_key.display()
|
||||||
|
);
|
||||||
// Authenticate with RSA key
|
// Authenticate with RSA key
|
||||||
if let Err(err) = session.userauth_pubkey_file(
|
if let Err(err) = session.userauth_pubkey_file(
|
||||||
username.as_str(),
|
username.as_str(),
|
||||||
@@ -321,6 +348,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
rsa_key.as_path(),
|
rsa_key.as_path(),
|
||||||
password.as_deref(),
|
password.as_deref(),
|
||||||
) {
|
) {
|
||||||
|
error!("Authentication failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::AuthenticationFailed,
|
FileTransferErrorType::AuthenticationFailed,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -329,10 +357,16 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Proceeed with username/password authentication
|
// Proceeed with username/password authentication
|
||||||
|
debug!(
|
||||||
|
"Authenticating with username {} and password {}",
|
||||||
|
username,
|
||||||
|
shadow_password(password.as_deref().unwrap_or(""))
|
||||||
|
);
|
||||||
if let Err(err) = session.userauth_password(
|
if let Err(err) = session.userauth_password(
|
||||||
username.as_str(),
|
username.as_str(),
|
||||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||||
) {
|
) {
|
||||||
|
error!("Authentication failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::AuthenticationFailed,
|
FileTransferErrorType::AuthenticationFailed,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -343,16 +377,19 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
// Set blocking to true
|
// Set blocking to true
|
||||||
session.set_blocking(true);
|
session.set_blocking(true);
|
||||||
// Get Sftp client
|
// Get Sftp client
|
||||||
|
debug!("Getting SFTP client...");
|
||||||
let sftp: Sftp = match session.sftp() {
|
let sftp: Sftp = match session.sftp() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not get sftp client: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ProtocolError,
|
FileTransferErrorType::ProtocolError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Get working directory
|
// Get working directory
|
||||||
|
debug!("Getting working directory...");
|
||||||
self.wrkdir = match sftp.realpath(PathBuf::from(".").as_path()) {
|
self.wrkdir = match sftp.realpath(PathBuf::from(".").as_path()) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -367,6 +404,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
self.session = Some(session);
|
self.session = Some(session);
|
||||||
// Set sftp
|
// Set sftp
|
||||||
self.sftp = Some(sftp);
|
self.sftp = Some(sftp);
|
||||||
|
info!(
|
||||||
|
"Connection established: {}; working directory {}",
|
||||||
|
banner.as_deref().unwrap_or(""),
|
||||||
|
self.wrkdir.display()
|
||||||
|
);
|
||||||
Ok(banner)
|
Ok(banner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,6 +416,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
///
|
///
|
||||||
/// Disconnect from the remote server
|
/// Disconnect from the remote server
|
||||||
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
||||||
|
info!("Disconnecting from remote...");
|
||||||
match self.session.as_ref() {
|
match self.session.as_ref() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
// Disconnect (greet server with 'Mandi' as they do in Friuli)
|
// Disconnect (greet server with 'Mandi' as they do in Friuli)
|
||||||
@@ -407,6 +450,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
///
|
///
|
||||||
/// Print working directory
|
/// Print working directory
|
||||||
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
||||||
|
info!("PWD: {}", self.wrkdir.display());
|
||||||
match self.sftp {
|
match self.sftp {
|
||||||
Some(_) => Ok(self.wrkdir.clone()),
|
Some(_) => Ok(self.wrkdir.clone()),
|
||||||
None => Err(FileTransferError::new(
|
None => Err(FileTransferError::new(
|
||||||
@@ -426,6 +470,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
info!("Changed working directory to {}", self.wrkdir.display());
|
||||||
Ok(self.wrkdir.clone())
|
Ok(self.wrkdir.clone())
|
||||||
}
|
}
|
||||||
None => Err(FileTransferError::new(
|
None => Err(FileTransferError::new(
|
||||||
@@ -437,11 +482,47 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
/// ### copy
|
/// ### copy
|
||||||
///
|
///
|
||||||
/// Copy file to destination
|
/// Copy file to destination
|
||||||
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> {
|
fn copy(&mut self, src: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
|
||||||
// SFTP doesn't support file copy
|
// NOTE: use SCP command to perform copy (UNSAFE)
|
||||||
Err(FileTransferError::new(
|
match self.is_connected() {
|
||||||
FileTransferErrorType::UnsupportedFeature,
|
true => {
|
||||||
))
|
let dst: PathBuf = self.get_abs_path(dst);
|
||||||
|
info!(
|
||||||
|
"Copying {} to {}",
|
||||||
|
src.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
|
// Run `cp -rf`
|
||||||
|
match self.perform_shell_cmd_with_path(
|
||||||
|
format!(
|
||||||
|
"cp -rf \"{}\" \"{}\"; echo $?",
|
||||||
|
src.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
) {
|
||||||
|
Ok(output) =>
|
||||||
|
// Check if output is 0
|
||||||
|
{
|
||||||
|
match output.as_str().trim() == "0" {
|
||||||
|
true => Ok(()), // File copied
|
||||||
|
false => Err(FileTransferError::new_ex(
|
||||||
|
// Could not copy file
|
||||||
|
FileTransferErrorType::FileCreateDenied,
|
||||||
|
format!("\"{}\"", dst.display()),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
|
FileTransferErrorType::ProtocolError,
|
||||||
|
err.to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false => Err(FileTransferError::new(
|
||||||
|
FileTransferErrorType::UninitializedSession,
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### list_dir
|
/// ### list_dir
|
||||||
@@ -455,6 +536,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
info!("Getting file entries in {}", path.display());
|
||||||
// Get files
|
// Get files
|
||||||
match sftp.readdir(dir.as_path()) {
|
match sftp.readdir(dir.as_path()) {
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -486,6 +568,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
Some(sftp) => {
|
Some(sftp) => {
|
||||||
// Make directory
|
// Make directory
|
||||||
let path: PathBuf = self.get_abs_path(PathBuf::from(dir).as_path());
|
let path: PathBuf = self.get_abs_path(PathBuf::from(dir).as_path());
|
||||||
|
info!("Making directory {}", path.display());
|
||||||
match sftp.mkdir(path.as_path(), 0o775) {
|
match sftp.mkdir(path.as_path(), 0o775) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -510,6 +593,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Match if file is a file or a directory
|
// Match if file is a file or a directory
|
||||||
|
info!("Removing file {}", file.get_abs_path().display());
|
||||||
match file {
|
match file {
|
||||||
FsEntry::File(f) => {
|
FsEntry::File(f) => {
|
||||||
// Remove file
|
// Remove file
|
||||||
@@ -523,6 +607,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
}
|
}
|
||||||
FsEntry::Directory(d) => {
|
FsEntry::Directory(d) => {
|
||||||
// Remove recursively
|
// Remove recursively
|
||||||
|
debug!("{} is a directory; removing all directory entries", d.name);
|
||||||
// Get directory files
|
// Get directory files
|
||||||
let directory_content: Vec<FsEntry> = match self.list_dir(d.abs_path.as_path()) {
|
let directory_content: Vec<FsEntry> = match self.list_dir(d.abs_path.as_path()) {
|
||||||
Ok(entries) => entries,
|
Ok(entries) => entries,
|
||||||
@@ -554,6 +639,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
FileTransferErrorType::UninitializedSession,
|
FileTransferErrorType::UninitializedSession,
|
||||||
)),
|
)),
|
||||||
Some(sftp) => {
|
Some(sftp) => {
|
||||||
|
info!(
|
||||||
|
"Moving {} to {}",
|
||||||
|
file.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
// Resolve destination path
|
// Resolve destination path
|
||||||
let abs_dst: PathBuf = self.get_abs_path(dst);
|
let abs_dst: PathBuf = self.get_abs_path(dst);
|
||||||
// Get abs path of entry
|
// Get abs path of entry
|
||||||
@@ -580,6 +670,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
info!("Stat file {}", dir.display());
|
||||||
// Get file
|
// Get file
|
||||||
match sftp.stat(dir.as_path()) {
|
match sftp.stat(dir.as_path()) {
|
||||||
Ok(metadata) => Ok(self.make_fsentry(dir.as_path(), &metadata)),
|
Ok(metadata) => Ok(self.make_fsentry(dir.as_path(), &metadata)),
|
||||||
@@ -599,6 +690,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
///
|
///
|
||||||
/// Execute a command on remote host
|
/// Execute a command on remote host
|
||||||
fn exec(&mut self, cmd: &str) -> Result<String, FileTransferError> {
|
fn exec(&mut self, cmd: &str) -> Result<String, FileTransferError> {
|
||||||
|
info!("Executing command {}", cmd);
|
||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => match self.perform_shell_cmd_with_path(cmd) {
|
true => match self.perform_shell_cmd_with_path(cmd) {
|
||||||
Ok(output) => Ok(output),
|
Ok(output) => Ok(output),
|
||||||
@@ -629,14 +721,20 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
)),
|
)),
|
||||||
Some(sftp) => {
|
Some(sftp) => {
|
||||||
let remote_path: PathBuf = self.get_abs_path(file_name);
|
let remote_path: PathBuf = self.get_abs_path(file_name);
|
||||||
|
info!(
|
||||||
|
"Sending file {} to {}",
|
||||||
|
local.abs_path.display(),
|
||||||
|
remote_path.display()
|
||||||
|
);
|
||||||
// Calculate file mode
|
// Calculate file mode
|
||||||
let mode: i32 = match local.unix_pex {
|
let mode: i32 = match local.unix_pex {
|
||||||
None => 0o644,
|
None => 0o644,
|
||||||
Some((u, g, o)) => ((u as i32) << 6) + ((g as i32) << 3) + (o as i32),
|
Some((u, g, o)) => ((u as i32) << 6) + ((g as i32) << 3) + (o as i32),
|
||||||
};
|
};
|
||||||
|
debug!("File mode {:?}", mode);
|
||||||
match sftp.open_mode(
|
match sftp.open_mode(
|
||||||
remote_path.as_path(),
|
remote_path.as_path(),
|
||||||
OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::APPEND | OpenFlags::TRUNCATE,
|
OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE,
|
||||||
mode,
|
mode,
|
||||||
OpenType::File,
|
OpenType::File,
|
||||||
) {
|
) {
|
||||||
@@ -664,6 +762,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
info!("Receiving file {}", remote_path.display());
|
||||||
// Open remote file
|
// Open remote file
|
||||||
match sftp.open(remote_path.as_path()) {
|
match sftp.open(remote_path.as_path()) {
|
||||||
Ok(file) => Ok(Box::new(BufReader::with_capacity(65536, file))),
|
Ok(file) => Ok(Box::new(BufReader::with_capacity(65536, file))),
|
||||||
@@ -702,6 +801,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_filetransfer_sftp_new() {
|
fn test_filetransfer_sftp_new() {
|
||||||
let client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
let client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fs_explorer_builder_new_default() {
|
fn test_fs_explorer_builder_new_default() {
|
||||||
let explorer: FileExplorer = FileExplorerBuilder::new().build();
|
let explorer: FileExplorer = FileExplorerBuilder::new().build();
|
||||||
|
|||||||
@@ -539,6 +539,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::fs::{FsDirectory, FsFile};
|
use crate::fs::{FsDirectory, FsFile};
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ mod tests {
|
|||||||
use crate::fs::{FsDirectory, FsFile};
|
use crate::fs::{FsDirectory, FsFile};
|
||||||
use crate::utils::fmt::fmt_time;
|
use crate::utils::fmt::fmt_time;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ impl FsEntry {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fs_fsentry_dir() {
|
fn test_fs_fsentry_dir() {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ pub enum HostErrorType {
|
|||||||
/// ### HostError
|
/// ### HostError
|
||||||
///
|
///
|
||||||
/// HostError is a wrapper for the error type and the exact io error
|
/// HostError is a wrapper for the error type and the exact io error
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct HostError {
|
pub struct HostError {
|
||||||
pub error: HostErrorType,
|
pub error: HostErrorType,
|
||||||
ioerr: Option<std::io::Error>,
|
ioerr: Option<std::io::Error>,
|
||||||
@@ -125,12 +125,17 @@ impl Localhost {
|
|||||||
///
|
///
|
||||||
/// Instantiates a new Localhost struct
|
/// Instantiates a new Localhost struct
|
||||||
pub fn new(wrkdir: PathBuf) -> Result<Localhost, HostError> {
|
pub fn new(wrkdir: PathBuf) -> Result<Localhost, HostError> {
|
||||||
|
debug!("Initializing localhost at {}", wrkdir.display());
|
||||||
let mut host: Localhost = Localhost {
|
let mut host: Localhost = Localhost {
|
||||||
wrkdir,
|
wrkdir,
|
||||||
files: Vec::new(),
|
files: Vec::new(),
|
||||||
};
|
};
|
||||||
// Check if dir exists
|
// Check if dir exists
|
||||||
if !host.file_exists(host.wrkdir.as_path()) {
|
if !host.file_exists(host.wrkdir.as_path()) {
|
||||||
|
error!(
|
||||||
|
"Failed to initialize localhost: {} doesn't exist",
|
||||||
|
host.wrkdir.display()
|
||||||
|
);
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
@@ -140,8 +145,15 @@ impl Localhost {
|
|||||||
// Retrieve files for provided path
|
// Retrieve files for provided path
|
||||||
host.files = match host.scan_dir(host.wrkdir.as_path()) {
|
host.files = match host.scan_dir(host.wrkdir.as_path()) {
|
||||||
Ok(files) => files,
|
Ok(files) => files,
|
||||||
Err(err) => return Err(err),
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"Failed to initialize localhost: could not scan wrkdir: {}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
info!("Localhost initialized with success");
|
||||||
Ok(host)
|
Ok(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +177,10 @@ impl Localhost {
|
|||||||
/// Change working directory with the new provided directory
|
/// Change working directory with the new provided directory
|
||||||
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
|
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
|
||||||
let new_dir: PathBuf = self.to_abs_path(new_dir);
|
let new_dir: PathBuf = self.to_abs_path(new_dir);
|
||||||
|
info!("Changing localhost directory to {}...", new_dir.display());
|
||||||
// Check whether directory exists
|
// Check whether directory exists
|
||||||
if !self.file_exists(new_dir.as_path()) {
|
if !self.file_exists(new_dir.as_path()) {
|
||||||
|
error!("Could not change directory: No such file or directory");
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
@@ -174,10 +188,11 @@ impl Localhost {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Change directory
|
// Change directory
|
||||||
if std::env::set_current_dir(new_dir.as_path()).is_err() {
|
if let Err(err) = std::env::set_current_dir(new_dir.as_path()) {
|
||||||
|
error!("Could not enter directory: {}", err);
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
Some(err),
|
||||||
new_dir.as_path(),
|
new_dir.as_path(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -189,11 +204,13 @@ impl Localhost {
|
|||||||
self.files = match self.scan_dir(self.wrkdir.as_path()) {
|
self.files = match self.scan_dir(self.wrkdir.as_path()) {
|
||||||
Ok(files) => files,
|
Ok(files) => files,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not scan new directory: {}", err);
|
||||||
// Restore directory
|
// Restore directory
|
||||||
self.wrkdir = prev_dir;
|
self.wrkdir = prev_dir;
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
debug!("Changed directory to {}", self.wrkdir.display());
|
||||||
Ok(self.wrkdir.clone())
|
Ok(self.wrkdir.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +227,7 @@ impl Localhost {
|
|||||||
/// ignex: don't report error if directory already exists
|
/// ignex: don't report error if directory already exists
|
||||||
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
|
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
|
||||||
let dir_path: PathBuf = self.to_abs_path(dir_name);
|
let dir_path: PathBuf = self.to_abs_path(dir_name);
|
||||||
|
info!("Making directory {}", dir_path.display());
|
||||||
// If dir already exists, return Error
|
// If dir already exists, return Error
|
||||||
if dir_path.exists() {
|
if dir_path.exists() {
|
||||||
match ignex {
|
match ignex {
|
||||||
@@ -229,13 +247,17 @@ impl Localhost {
|
|||||||
if dir_name.is_relative() {
|
if dir_name.is_relative() {
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
}
|
}
|
||||||
|
info!("Created directory {}", dir_path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::CouldNotCreateFile,
|
error!("Could not make directory: {}", err);
|
||||||
Some(err),
|
Err(HostError::new(
|
||||||
dir_path.as_path(),
|
HostErrorType::CouldNotCreateFile,
|
||||||
)),
|
Some(err),
|
||||||
|
dir_path.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +268,9 @@ impl Localhost {
|
|||||||
match entry {
|
match entry {
|
||||||
FsEntry::Directory(dir) => {
|
FsEntry::Directory(dir) => {
|
||||||
// If file doesn't exist; return error
|
// If file doesn't exist; return error
|
||||||
|
debug!("Removing directory {}", dir.abs_path.display());
|
||||||
if !dir.abs_path.as_path().exists() {
|
if !dir.abs_path.as_path().exists() {
|
||||||
|
error!("Directory doesn't exist");
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
@@ -258,18 +282,24 @@ impl Localhost {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Update dir
|
// Update dir
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
|
info!("Removed directory {}", dir.abs_path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::DeleteFailed,
|
error!("Could not remove directory: {}", err);
|
||||||
Some(err),
|
Err(HostError::new(
|
||||||
dir.abs_path.as_path(),
|
HostErrorType::DeleteFailed,
|
||||||
)),
|
Some(err),
|
||||||
|
dir.abs_path.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FsEntry::File(file) => {
|
FsEntry::File(file) => {
|
||||||
// If file doesn't exist; return error
|
// If file doesn't exist; return error
|
||||||
|
debug!("Removing file {}", file.abs_path.display());
|
||||||
if !file.abs_path.as_path().exists() {
|
if !file.abs_path.as_path().exists() {
|
||||||
|
error!("File doesn't exist");
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
@@ -281,13 +311,17 @@ impl Localhost {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Update dir
|
// Update dir
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
|
info!("Removed file {}", file.abs_path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::DeleteFailed,
|
error!("Could not remove file: {}", err);
|
||||||
Some(err),
|
Err(HostError::new(
|
||||||
file.abs_path.as_path(),
|
HostErrorType::DeleteFailed,
|
||||||
)),
|
Some(err),
|
||||||
|
file.abs_path.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,13 +336,26 @@ impl Localhost {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Scan dir
|
// Scan dir
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
|
debug!(
|
||||||
|
"Moved file {} to {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dst_path.display()
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::CouldNotCreateFile,
|
error!(
|
||||||
Some(err),
|
"Failed to move {} to {}: {}",
|
||||||
abs_path.as_path(),
|
entry.get_abs_path().display(),
|
||||||
)),
|
dst_path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
Err(HostError::new(
|
||||||
|
HostErrorType::CouldNotCreateFile,
|
||||||
|
Some(err),
|
||||||
|
abs_path.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,6 +365,11 @@ impl Localhost {
|
|||||||
pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> {
|
pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> {
|
||||||
// Get absolute path of dest
|
// Get absolute path of dest
|
||||||
let dst: PathBuf = self.to_abs_path(dst);
|
let dst: PathBuf = self.to_abs_path(dst);
|
||||||
|
info!(
|
||||||
|
"Copying file {} to {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
// Match entry
|
// Match entry
|
||||||
match entry {
|
match entry {
|
||||||
FsEntry::File(file) => {
|
FsEntry::File(file) => {
|
||||||
@@ -333,16 +385,19 @@ impl Localhost {
|
|||||||
};
|
};
|
||||||
// Copy entry path to dst path
|
// Copy entry path to dst path
|
||||||
if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) {
|
if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) {
|
||||||
|
error!("Failed to copy file: {}", err);
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::CouldNotCreateFile,
|
HostErrorType::CouldNotCreateFile,
|
||||||
Some(err),
|
Some(err),
|
||||||
file.abs_path.as_path(),
|
file.abs_path.as_path(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
info!("File copied");
|
||||||
}
|
}
|
||||||
FsEntry::Directory(dir) => {
|
FsEntry::Directory(dir) => {
|
||||||
// If destination path doesn't exist, create destination
|
// If destination path doesn't exist, create destination
|
||||||
if !dst.exists() {
|
if !dst.exists() {
|
||||||
|
debug!("Directory {} doesn't exist; creating it", dst.display());
|
||||||
self.mkdir(dst.as_path())?;
|
self.mkdir(dst.as_path())?;
|
||||||
}
|
}
|
||||||
// Scan dir
|
// Scan dir
|
||||||
@@ -386,15 +441,17 @@ impl Localhost {
|
|||||||
/// Stat file and create a FsEntry
|
/// Stat file and create a FsEntry
|
||||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
||||||
|
info!("Stating file {}", path.display());
|
||||||
let path: PathBuf = self.to_abs_path(path);
|
let path: PathBuf = self.to_abs_path(path);
|
||||||
let attr: Metadata = match fs::metadata(path.as_path()) {
|
let attr: Metadata = match fs::metadata(path.as_path()) {
|
||||||
Ok(metadata) => metadata,
|
Ok(metadata) => metadata,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not read file metadata: {}", err);
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::FileNotAccessible,
|
HostErrorType::FileNotAccessible,
|
||||||
Some(err),
|
Some(err),
|
||||||
path.as_path(),
|
path.as_path(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
||||||
@@ -454,14 +511,16 @@ impl Localhost {
|
|||||||
#[cfg(not(tarpaulin_include))]
|
#[cfg(not(tarpaulin_include))]
|
||||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
||||||
let path: PathBuf = self.to_abs_path(path);
|
let path: PathBuf = self.to_abs_path(path);
|
||||||
|
info!("Stating file {}", path.display());
|
||||||
let attr: Metadata = match fs::metadata(path.as_path()) {
|
let attr: Metadata = match fs::metadata(path.as_path()) {
|
||||||
Ok(metadata) => metadata,
|
Ok(metadata) => metadata,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not read file metadata: {}", err);
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::FileNotAccessible,
|
HostErrorType::FileNotAccessible,
|
||||||
Some(err),
|
Some(err),
|
||||||
path.as_path(),
|
path.as_path(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
||||||
@@ -523,16 +582,23 @@ impl Localhost {
|
|||||||
let args: Vec<&str> = cmd.split(' ').collect();
|
let args: Vec<&str> = cmd.split(' ').collect();
|
||||||
let cmd: &str = args.first().unwrap();
|
let cmd: &str = args.first().unwrap();
|
||||||
let argv: &[&str] = &args[1..];
|
let argv: &[&str] = &args[1..];
|
||||||
|
info!("Executing command: {} {:?}", cmd, argv);
|
||||||
match std::process::Command::new(cmd).args(argv).output() {
|
match std::process::Command::new(cmd).args(argv).output() {
|
||||||
Ok(output) => match std::str::from_utf8(&output.stdout) {
|
Ok(output) => match std::str::from_utf8(&output.stdout) {
|
||||||
Ok(s) => Ok(s.to_string()),
|
Ok(s) => {
|
||||||
|
info!("Command output: {}", s);
|
||||||
|
Ok(s.to_string())
|
||||||
|
}
|
||||||
Err(_) => Ok(String::new()),
|
Err(_) => Ok(String::new()),
|
||||||
},
|
},
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::ExecutionFailed,
|
error!("Failed to run command: {}", err);
|
||||||
Some(err),
|
Err(HostError::new(
|
||||||
self.wrkdir.as_path(),
|
HostErrorType::ExecutionFailed,
|
||||||
)),
|
Some(err),
|
||||||
|
self.wrkdir.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,19 +614,32 @@ impl Localhost {
|
|||||||
let mut mpex = metadata.permissions();
|
let mut mpex = metadata.permissions();
|
||||||
mpex.set_mode(self.mode_to_u32(pex));
|
mpex.set_mode(self.mode_to_u32(pex));
|
||||||
match set_permissions(path.as_path(), mpex) {
|
match set_permissions(path.as_path(), mpex) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => {
|
||||||
Err(err) => Err(HostError::new(
|
info!("Changed mode for {} to {:?}", path.display(), pex);
|
||||||
HostErrorType::FileNotAccessible,
|
Ok(())
|
||||||
Some(err),
|
}
|
||||||
path.as_path(),
|
Err(err) => {
|
||||||
)),
|
error!("Could not change mode for file {}: {}", path.display(), err);
|
||||||
|
Err(HostError::new(
|
||||||
|
HostErrorType::FileNotAccessible,
|
||||||
|
Some(err),
|
||||||
|
path.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::FileNotAccessible,
|
error!(
|
||||||
Some(err),
|
"Chmod failed; could not read metadata for file {}: {}",
|
||||||
path.as_path(),
|
path.display(),
|
||||||
)),
|
err
|
||||||
|
);
|
||||||
|
Err(HostError::new(
|
||||||
|
HostErrorType::FileNotAccessible,
|
||||||
|
Some(err),
|
||||||
|
path.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,7 +648,9 @@ impl Localhost {
|
|||||||
/// Open file for read
|
/// Open file for read
|
||||||
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> {
|
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> {
|
||||||
let file: PathBuf = self.to_abs_path(file);
|
let file: PathBuf = self.to_abs_path(file);
|
||||||
|
info!("Opening file {} for read", file.display());
|
||||||
if !self.file_exists(file.as_path()) {
|
if !self.file_exists(file.as_path()) {
|
||||||
|
error!("File doesn't exist!");
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
@@ -583,11 +664,14 @@ impl Localhost {
|
|||||||
.open(file.as_path())
|
.open(file.as_path())
|
||||||
{
|
{
|
||||||
Ok(f) => Ok(f),
|
Ok(f) => Ok(f),
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::FileNotAccessible,
|
error!("Could not open file for read: {}", err);
|
||||||
Some(err),
|
Err(HostError::new(
|
||||||
file.as_path(),
|
HostErrorType::FileNotAccessible,
|
||||||
)),
|
Some(err),
|
||||||
|
file.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,6 +680,7 @@ impl Localhost {
|
|||||||
/// Open file for write
|
/// Open file for write
|
||||||
pub fn open_file_write(&self, file: &Path) -> Result<File, HostError> {
|
pub fn open_file_write(&self, file: &Path) -> Result<File, HostError> {
|
||||||
let file: PathBuf = self.to_abs_path(file);
|
let file: PathBuf = self.to_abs_path(file);
|
||||||
|
info!("Opening file {} for write", file.display());
|
||||||
match OpenOptions::new()
|
match OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
@@ -603,18 +688,21 @@ impl Localhost {
|
|||||||
.open(file.as_path())
|
.open(file.as_path())
|
||||||
{
|
{
|
||||||
Ok(f) => Ok(f),
|
Ok(f) => Ok(f),
|
||||||
Err(err) => match self.file_exists(file.as_path()) {
|
Err(err) => {
|
||||||
true => Err(HostError::new(
|
error!("Failed to open file: {}", err);
|
||||||
HostErrorType::ReadonlyFile,
|
match self.file_exists(file.as_path()) {
|
||||||
Some(err),
|
true => Err(HostError::new(
|
||||||
file.as_path(),
|
HostErrorType::ReadonlyFile,
|
||||||
)),
|
Some(err),
|
||||||
false => Err(HostError::new(
|
file.as_path(),
|
||||||
HostErrorType::FileNotAccessible,
|
)),
|
||||||
Some(err),
|
false => Err(HostError::new(
|
||||||
file.as_path(),
|
HostErrorType::FileNotAccessible,
|
||||||
)),
|
Some(err),
|
||||||
},
|
file.as_path(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,13 +717,15 @@ impl Localhost {
|
|||||||
///
|
///
|
||||||
/// Get content of the current directory as a list of fs entry
|
/// Get content of the current directory as a list of fs entry
|
||||||
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<FsEntry>, HostError> {
|
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<FsEntry>, HostError> {
|
||||||
|
info!("Reading directory {}", dir.display());
|
||||||
match std::fs::read_dir(dir) {
|
match std::fs::read_dir(dir) {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
let mut fs_entries: Vec<FsEntry> = Vec::new();
|
let mut fs_entries: Vec<FsEntry> = Vec::new();
|
||||||
for entry in e.flatten() {
|
for entry in e.flatten() {
|
||||||
// NOTE: 0.4.1, don't fail if stat for one file fails
|
// NOTE: 0.4.1, don't fail if stat for one file fails
|
||||||
if let Ok(entry) = self.stat(entry.path().as_path()) {
|
match self.stat(entry.path().as_path()) {
|
||||||
fs_entries.push(entry);
|
Ok(entry) => fs_entries.push(entry),
|
||||||
|
Err(e) => error!("Failed to stat {}: {}", entry.path().display(), e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(fs_entries)
|
Ok(fs_entries)
|
||||||
@@ -739,6 +829,8 @@ impl Localhost {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
#![doc(html_playground_url = "https://play.rust-lang.org")]
|
||||||
|
#![doc(
|
||||||
|
html_favicon_url = "https://raw.githubusercontent.com/veeso/termscp/main/assets/images/termscp-128.png"
|
||||||
|
)]
|
||||||
|
#![doc(
|
||||||
|
html_logo_url = "https://raw.githubusercontent.com/veeso/termscp/main/assets/images/termscp-512.png"
|
||||||
|
)]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MIT License
|
* MIT License
|
||||||
*
|
*
|
||||||
@@ -27,6 +35,8 @@ extern crate bitflags;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
#[macro_use]
|
||||||
extern crate magic_crypt;
|
extern crate magic_crypt;
|
||||||
|
|
||||||
pub mod activity_manager;
|
pub mod activity_manager;
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ extern crate bitflags;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
#[macro_use]
|
||||||
extern crate magic_crypt;
|
extern crate magic_crypt;
|
||||||
extern crate rpassword;
|
extern crate rpassword;
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@ mod utils;
|
|||||||
// namespaces
|
// namespaces
|
||||||
use activity_manager::{ActivityManager, NextActivity};
|
use activity_manager::{ActivityManager, NextActivity};
|
||||||
use filetransfer::FileTransferProtocol;
|
use filetransfer::FileTransferProtocol;
|
||||||
|
use system::logging;
|
||||||
|
|
||||||
/// ### print_usage
|
/// ### print_usage
|
||||||
///
|
///
|
||||||
@@ -66,6 +69,7 @@ fn print_usage(opts: Options) {
|
|||||||
);
|
);
|
||||||
print!("{}", opts.usage(&brief));
|
print!("{}", opts.usage(&brief));
|
||||||
println!("\nPlease, report issues to <https://github.com/veeso/termscp>");
|
println!("\nPlease, report issues to <https://github.com/veeso/termscp>");
|
||||||
|
println!("Please, consider supporting the author <https://www.buymeacoffee.com/veeso>")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -78,15 +82,12 @@ fn main() {
|
|||||||
let mut remote_wrkdir: Option<PathBuf> = None;
|
let mut remote_wrkdir: Option<PathBuf> = None;
|
||||||
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
|
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
|
||||||
let mut ticks: Duration = Duration::from_millis(10);
|
let mut ticks: Duration = Duration::from_millis(10);
|
||||||
|
let mut log_enabled: bool = true;
|
||||||
//Process options
|
//Process options
|
||||||
let mut opts = Options::new();
|
let mut opts = Options::new();
|
||||||
opts.optopt(
|
opts.optopt("P", "password", "Provide password from CLI", "<password>");
|
||||||
"P",
|
|
||||||
"password",
|
|
||||||
"Provide password from CLI (use at your own risk)",
|
|
||||||
"<password>",
|
|
||||||
);
|
|
||||||
opts.optopt("T", "ticks", "Set UI ticks; default 10ms", "<ms>");
|
opts.optopt("T", "ticks", "Set UI ticks; default 10ms", "<ms>");
|
||||||
|
opts.optflag("q", "quiet", "Disable logging");
|
||||||
opts.optflag("v", "version", "");
|
opts.optflag("v", "version", "");
|
||||||
opts.optflag("h", "help", "Print this menu");
|
opts.optflag("h", "help", "Print this menu");
|
||||||
let matches = match opts.parse(&args[1..]) {
|
let matches = match opts.parse(&args[1..]) {
|
||||||
@@ -104,11 +105,15 @@ fn main() {
|
|||||||
// Version
|
// Version
|
||||||
if matches.opt_present("v") {
|
if matches.opt_present("v") {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"TermSCP - {} - Developed by {}",
|
"termscp - {} - Developed by {}",
|
||||||
TERMSCP_VERSION, TERMSCP_AUTHORS,
|
TERMSCP_VERSION, TERMSCP_AUTHORS,
|
||||||
);
|
);
|
||||||
std::process::exit(255);
|
std::process::exit(255);
|
||||||
}
|
}
|
||||||
|
// Logging
|
||||||
|
if matches.opt_present("q") {
|
||||||
|
log_enabled = false;
|
||||||
|
}
|
||||||
// Match password
|
// Match password
|
||||||
if let Some(passwd) = matches.opt_str("P") {
|
if let Some(passwd) = matches.opt_str("P") {
|
||||||
password = Some(passwd);
|
password = Some(passwd);
|
||||||
@@ -159,9 +164,17 @@ fn main() {
|
|||||||
Ok(dir) => dir,
|
Ok(dir) => dir,
|
||||||
Err(_) => PathBuf::from("/"),
|
Err(_) => PathBuf::from("/"),
|
||||||
};
|
};
|
||||||
|
// Setup logging
|
||||||
|
if log_enabled {
|
||||||
|
if let Err(err) = logging::init() {
|
||||||
|
eprintln!("Failed to initialize logging: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("termscp {} started!", TERMSCP_VERSION);
|
||||||
// Initialize client if necessary
|
// Initialize client if necessary
|
||||||
let mut start_activity: NextActivity = NextActivity::Authentication;
|
let mut start_activity: NextActivity = NextActivity::Authentication;
|
||||||
if address.is_some() {
|
if address.is_some() {
|
||||||
|
debug!("User has specified remote options: address: {:?}, port: {:?}, protocol: {:?}, user: {:?}, password: {}", address, port, protocol, username, utils::fmt::shadow_password(password.as_deref().unwrap_or("")));
|
||||||
if password.is_none() {
|
if password.is_none() {
|
||||||
// Ask password if unspecified
|
// Ask password if unspecified
|
||||||
password = match rpassword::read_password_from_tty(Some("Password: ")) {
|
password = match rpassword::read_password_from_tty(Some("Password: ")) {
|
||||||
@@ -177,6 +190,10 @@ fn main() {
|
|||||||
std::process::exit(255);
|
std::process::exit(255);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
debug!(
|
||||||
|
"Read password from tty: {}",
|
||||||
|
utils::fmt::shadow_password(password.as_deref().unwrap_or(""))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// In this case the first activity will be FileTransfer
|
// In this case the first activity will be FileTransfer
|
||||||
start_activity = NextActivity::FileTransfer;
|
start_activity = NextActivity::FileTransfer;
|
||||||
@@ -194,7 +211,9 @@ fn main() {
|
|||||||
manager.set_filetransfer_params(address, port, protocol, username, password, remote_wrkdir);
|
manager.set_filetransfer_params(address, port, protocol, username, password, remote_wrkdir);
|
||||||
}
|
}
|
||||||
// Run
|
// Run
|
||||||
|
info!("Starting activity manager...");
|
||||||
manager.run(start_activity);
|
manager.run(start_activity);
|
||||||
|
info!("termscp terminated");
|
||||||
// Then return
|
// Then return
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,9 +68,11 @@ impl BookmarksClient {
|
|||||||
) -> Result<BookmarksClient, SerializerError> {
|
) -> Result<BookmarksClient, SerializerError> {
|
||||||
// Create default hosts
|
// Create default hosts
|
||||||
let default_hosts: UserHosts = Default::default();
|
let default_hosts: UserHosts = Default::default();
|
||||||
|
debug!("Setting up bookmarks client...");
|
||||||
// Make a key storage (windows / macos)
|
// Make a key storage (windows / macos)
|
||||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||||
|
debug!("Setting up KeyStorage");
|
||||||
let username: String = whoami::username();
|
let username: String = whoami::username();
|
||||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||||
// Check if keyring storage is supported
|
// Check if keyring storage is supported
|
||||||
@@ -79,8 +81,14 @@ impl BookmarksClient {
|
|||||||
#[cfg(test)] // NOTE: when running test, add -test
|
#[cfg(test)] // NOTE: when running test, add -test
|
||||||
let app_name: &str = "termscp-test";
|
let app_name: &str = "termscp-test";
|
||||||
match storage.is_supported() {
|
match storage.is_supported() {
|
||||||
true => (Box::new(storage), app_name),
|
true => {
|
||||||
false => (Box::new(FileStorage::new(storage_path)), "bookmarks"),
|
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 (linux / unix)
|
// Make a key storage (linux / unix)
|
||||||
@@ -90,16 +98,22 @@ impl BookmarksClient {
|
|||||||
let app_name: &str = "bookmarks";
|
let app_name: &str = "bookmarks";
|
||||||
#[cfg(test)] // NOTE: when running test, add -test
|
#[cfg(test)] // NOTE: when running test, add -test
|
||||||
let app_name: &str = "bookmarks-test";
|
let app_name: &str = "bookmarks-test";
|
||||||
|
debug!("Using FileStorage");
|
||||||
(Box::new(FileStorage::new(storage_path)), app_name)
|
(Box::new(FileStorage::new(storage_path)), app_name)
|
||||||
};
|
};
|
||||||
// Load key
|
// Load key
|
||||||
let key: String = match key_storage.get_key(service_id) {
|
let key: String = match key_storage.get_key(service_id) {
|
||||||
Ok(k) => k,
|
Ok(k) => {
|
||||||
|
debug!("Key loaded with success");
|
||||||
|
k
|
||||||
|
}
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
KeyStorageError::NoSuchKey => {
|
KeyStorageError::NoSuchKey => {
|
||||||
// If no such key, generate key and set it into the storage
|
// If no such key, generate key and set it into the storage
|
||||||
let key: String = Self::generate_key();
|
let key: String = Self::generate_key();
|
||||||
|
debug!("Key doesn't exist yet or could not be loaded; generated a new key");
|
||||||
if let Err(e) = key_storage.set_key(service_id, key.as_str()) {
|
if let Err(e) = key_storage.set_key(service_id, key.as_str()) {
|
||||||
|
error!("Failed to set new key into storage: {}", e);
|
||||||
return Err(SerializerError::new_ex(
|
return Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::IoError,
|
SerializerErrorKind::IoError,
|
||||||
format!("Could not write key to storage: {}", e),
|
format!("Could not write key to storage: {}", e),
|
||||||
@@ -109,10 +123,11 @@ impl BookmarksClient {
|
|||||||
key
|
key
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
error!("Failed to get key from storage: {}", e);
|
||||||
return Err(SerializerError::new_ex(
|
return Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::IoError,
|
SerializerErrorKind::IoError,
|
||||||
format!("Could not get key from storage: {}", e),
|
format!("Could not get key from storage: {}", e),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -124,15 +139,19 @@ impl BookmarksClient {
|
|||||||
};
|
};
|
||||||
// If bookmark file doesn't exist, initialize it
|
// If bookmark file doesn't exist, initialize it
|
||||||
if !bookmarks_file.exists() {
|
if !bookmarks_file.exists() {
|
||||||
|
info!("Bookmarks file doesn't exist yet; creating it...");
|
||||||
if let Err(err) = client.write_bookmarks() {
|
if let Err(err) = client.write_bookmarks() {
|
||||||
|
error!("Failed to create bookmarks file: {}", err);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Load bookmarks from file
|
// Load bookmarks from file
|
||||||
if let Err(err) = client.read_bookmarks() {
|
if let Err(err) = client.read_bookmarks() {
|
||||||
|
error!("Failed to load bookmarks: {}", err);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
info!("Bookmarks client initialized");
|
||||||
// Load key
|
// Load key
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
@@ -152,19 +171,29 @@ impl BookmarksClient {
|
|||||||
key: &str,
|
key: &str,
|
||||||
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
|
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
|
||||||
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
|
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
|
||||||
|
debug!("Getting bookmark {}", key);
|
||||||
Some((
|
Some((
|
||||||
entry.address.clone(),
|
entry.address.clone(),
|
||||||
entry.port,
|
entry.port,
|
||||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||||
Ok(proto) => proto,
|
Ok(proto) => proto,
|
||||||
Err(_) => FileTransferProtocol::Sftp, // Default
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
FileTransferProtocol::Sftp // Default
|
||||||
|
}
|
||||||
},
|
},
|
||||||
entry.username.clone(),
|
entry.username.clone(),
|
||||||
match &entry.password {
|
match &entry.password {
|
||||||
// Decrypted password if Some; if decryption fails return None
|
// Decrypted password if Some; if decryption fails return None
|
||||||
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
|
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
|
||||||
Ok(decrypted_pwd) => Some(decrypted_pwd),
|
Ok(decrypted_pwd) => Some(decrypted_pwd),
|
||||||
Err(_) => None,
|
Err(err) => {
|
||||||
|
error!("Failed to decrypt password for bookmark: {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
},
|
},
|
||||||
@@ -184,9 +213,11 @@ impl BookmarksClient {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) {
|
) {
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
|
error!("Fatal error; bookmark name is empty");
|
||||||
panic!("Bookmark name can't be empty");
|
panic!("Bookmark name can't be empty");
|
||||||
}
|
}
|
||||||
// Make bookmark
|
// Make bookmark
|
||||||
|
info!("Added bookmark {} with address {}", name, addr);
|
||||||
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
|
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
|
||||||
self.hosts.bookmarks.insert(name, host);
|
self.hosts.bookmarks.insert(name, host);
|
||||||
}
|
}
|
||||||
@@ -196,6 +227,7 @@ impl BookmarksClient {
|
|||||||
/// Delete entry from bookmarks
|
/// Delete entry from bookmarks
|
||||||
pub fn del_bookmark(&mut self, name: &str) {
|
pub fn del_bookmark(&mut self, name: &str) {
|
||||||
let _ = self.hosts.bookmarks.remove(name);
|
let _ = self.hosts.bookmarks.remove(name);
|
||||||
|
info!("Removed bookmark {}", name);
|
||||||
}
|
}
|
||||||
/// ### iter_recents
|
/// ### iter_recents
|
||||||
///
|
///
|
||||||
@@ -209,13 +241,20 @@ impl BookmarksClient {
|
|||||||
/// Get recent associated to key
|
/// Get recent associated to key
|
||||||
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
|
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
|
||||||
// NOTE: password is not decrypted; recents will never have password
|
// NOTE: password is not decrypted; recents will never have password
|
||||||
|
info!("Getting bookmark {}", key);
|
||||||
let entry: &Bookmark = self.hosts.recents.get(key)?;
|
let entry: &Bookmark = self.hosts.recents.get(key)?;
|
||||||
Some((
|
Some((
|
||||||
entry.address.clone(),
|
entry.address.clone(),
|
||||||
entry.port,
|
entry.port,
|
||||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||||
Ok(proto) => proto,
|
Ok(proto) => proto,
|
||||||
Err(_) => FileTransferProtocol::Sftp, // Default
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
FileTransferProtocol::Sftp // Default
|
||||||
|
}
|
||||||
},
|
},
|
||||||
entry.username.clone(),
|
entry.username.clone(),
|
||||||
))
|
))
|
||||||
@@ -236,6 +275,7 @@ impl BookmarksClient {
|
|||||||
// Check if duplicated
|
// Check if duplicated
|
||||||
for recent_host in self.hosts.recents.values() {
|
for recent_host in self.hosts.recents.values() {
|
||||||
if *recent_host == host {
|
if *recent_host == host {
|
||||||
|
debug!("Discarding recent since duplicated ({})", host.address);
|
||||||
// Don't save duplicates
|
// Don't save duplicates
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -252,6 +292,7 @@ impl BookmarksClient {
|
|||||||
// Delete keys starting from the last one
|
// Delete keys starting from the last one
|
||||||
for key in keys.iter() {
|
for key in keys.iter() {
|
||||||
let _ = self.hosts.recents.remove(key);
|
let _ = self.hosts.recents.remove(key);
|
||||||
|
debug!("Removed recent bookmark {}", key);
|
||||||
// If length is < self.recents_size; break
|
// If length is < self.recents_size; break
|
||||||
if self.hosts.recents.len() < self.recents_size {
|
if self.hosts.recents.len() < self.recents_size {
|
||||||
break;
|
break;
|
||||||
@@ -259,6 +300,7 @@ impl BookmarksClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
||||||
|
info!("Saved recent host {} ({})", name, host.address);
|
||||||
self.hosts.recents.insert(name, host);
|
self.hosts.recents.insert(name, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,6 +309,7 @@ impl BookmarksClient {
|
|||||||
/// Delete entry from recents
|
/// Delete entry from recents
|
||||||
pub fn del_recent(&mut self, name: &str) {
|
pub fn del_recent(&mut self, name: &str) {
|
||||||
let _ = self.hosts.recents.remove(name);
|
let _ = self.hosts.recents.remove(name);
|
||||||
|
info!("Removed recent host {}", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### write_bookmarks
|
/// ### write_bookmarks
|
||||||
@@ -274,6 +317,7 @@ impl BookmarksClient {
|
|||||||
/// Write bookmarks to file
|
/// Write bookmarks to file
|
||||||
pub fn write_bookmarks(&self) -> Result<(), SerializerError> {
|
pub fn write_bookmarks(&self) -> Result<(), SerializerError> {
|
||||||
// Open file
|
// Open file
|
||||||
|
debug!("Writing bookmarks");
|
||||||
match OpenOptions::new()
|
match OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
@@ -284,10 +328,13 @@ impl BookmarksClient {
|
|||||||
let serializer: BookmarkSerializer = BookmarkSerializer {};
|
let serializer: BookmarkSerializer = BookmarkSerializer {};
|
||||||
serializer.serialize(Box::new(writer), &self.hosts)
|
serializer.serialize(Box::new(writer), &self.hosts)
|
||||||
}
|
}
|
||||||
Err(err) => Err(SerializerError::new_ex(
|
Err(err) => {
|
||||||
SerializerErrorKind::IoError,
|
error!("Failed to write bookmarks: {}", err);
|
||||||
err.to_string(),
|
Err(SerializerError::new_ex(
|
||||||
)),
|
SerializerErrorKind::IoError,
|
||||||
|
err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +343,7 @@ impl BookmarksClient {
|
|||||||
/// Read bookmarks from file
|
/// Read bookmarks from file
|
||||||
fn read_bookmarks(&mut self) -> Result<(), SerializerError> {
|
fn read_bookmarks(&mut self) -> Result<(), SerializerError> {
|
||||||
// Open bookmarks file for read
|
// Open bookmarks file for read
|
||||||
|
debug!("Reading bookmarks");
|
||||||
match OpenOptions::new()
|
match OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
.open(self.bookmarks_file.as_path())
|
.open(self.bookmarks_file.as_path())
|
||||||
@@ -311,10 +359,13 @@ impl BookmarksClient {
|
|||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => Err(SerializerError::new_ex(
|
Err(err) => {
|
||||||
SerializerErrorKind::IoError,
|
error!("Failed to read bookmarks: {}", err);
|
||||||
err.to_string(),
|
Err(SerializerError::new_ex(
|
||||||
)),
|
SerializerErrorKind::IoError,
|
||||||
|
err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +423,8 @@ impl BookmarksClient {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ impl ConfigClient {
|
|||||||
pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result<ConfigClient, SerializerError> {
|
pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result<ConfigClient, SerializerError> {
|
||||||
// Initialize a default configuration
|
// Initialize a default configuration
|
||||||
let default_config: UserConfig = UserConfig::default();
|
let default_config: UserConfig = UserConfig::default();
|
||||||
|
info!(
|
||||||
|
"Setting up config client with config path {} and SSH key directory {}",
|
||||||
|
config_path.display(),
|
||||||
|
ssh_key_dir.display()
|
||||||
|
);
|
||||||
// Create client
|
// Create client
|
||||||
let mut client: ConfigClient = ConfigClient {
|
let mut client: ConfigClient = ConfigClient {
|
||||||
config: default_config,
|
config: default_config,
|
||||||
@@ -67,6 +72,7 @@ impl ConfigClient {
|
|||||||
// If ssh key directory doesn't exist, create it
|
// If ssh key directory doesn't exist, create it
|
||||||
if !ssh_key_dir.exists() {
|
if !ssh_key_dir.exists() {
|
||||||
if let Err(err) = create_dir(ssh_key_dir) {
|
if let Err(err) = create_dir(ssh_key_dir) {
|
||||||
|
error!("Failed to create SSH key dir: {}", err);
|
||||||
return Err(SerializerError::new_ex(
|
return Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::IoError,
|
SerializerErrorKind::IoError,
|
||||||
format!(
|
format!(
|
||||||
@@ -76,17 +82,22 @@ impl ConfigClient {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
debug!("Created SSH key directory");
|
||||||
}
|
}
|
||||||
// If Config file doesn't exist, create it
|
// If Config file doesn't exist, create it
|
||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
if let Err(err) = client.write_config() {
|
if let Err(err) = client.write_config() {
|
||||||
|
error!("Couldn't create configuration file: {}", err);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
debug!("Config file didn't exist; created file");
|
||||||
} else {
|
} else {
|
||||||
// otherwise Load configuration from file
|
// otherwise Load configuration from file
|
||||||
if let Err(err) = client.read_config() {
|
if let Err(err) = client.read_config() {
|
||||||
|
error!("Couldn't read configuration file: {}", err);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
debug!("Read configuration file");
|
||||||
}
|
}
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
@@ -176,23 +187,40 @@ impl ConfigClient {
|
|||||||
self.config.user_interface.group_dirs = val.map(|val| val.to_string());
|
self.config.user_interface.group_dirs = val.map(|val| val.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get_file_fmt
|
/// ### get_local_file_fmt
|
||||||
///
|
///
|
||||||
/// Get current file fmt
|
/// Get current file fmt for local host
|
||||||
pub fn get_file_fmt(&self) -> Option<String> {
|
pub fn get_local_file_fmt(&self) -> Option<String> {
|
||||||
self.config.user_interface.file_fmt.clone()
|
self.config.user_interface.file_fmt.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### set_file_fmt
|
/// ### set_local_file_fmt
|
||||||
///
|
///
|
||||||
/// Set file fmt parameter
|
/// Set file fmt parameter for local host
|
||||||
pub fn set_file_fmt(&mut self, s: String) {
|
pub fn set_local_file_fmt(&mut self, s: String) {
|
||||||
self.config.user_interface.file_fmt = match s.is_empty() {
|
self.config.user_interface.file_fmt = match s.is_empty() {
|
||||||
true => None,
|
true => None,
|
||||||
false => Some(s),
|
false => Some(s),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### get_remote_file_fmt
|
||||||
|
///
|
||||||
|
/// Get current file fmt for remote host
|
||||||
|
pub fn get_remote_file_fmt(&self) -> Option<String> {
|
||||||
|
self.config.user_interface.remote_file_fmt.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### set_remote_file_fmt
|
||||||
|
///
|
||||||
|
/// Set file fmt parameter for remote host
|
||||||
|
pub fn set_remote_file_fmt(&mut self, s: String) {
|
||||||
|
self.config.user_interface.remote_file_fmt = match s.is_empty() {
|
||||||
|
true => None,
|
||||||
|
false => Some(s),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// SSH Keys
|
// SSH Keys
|
||||||
|
|
||||||
/// ### save_ssh_key
|
/// ### save_ssh_key
|
||||||
@@ -213,12 +241,18 @@ impl ConfigClient {
|
|||||||
p.push(format!("{}.key", host_name));
|
p.push(format!("{}.key", host_name));
|
||||||
p
|
p
|
||||||
};
|
};
|
||||||
|
info!(
|
||||||
|
"Writing SSH file to {} for host {}",
|
||||||
|
ssh_key_path.display(),
|
||||||
|
host_name
|
||||||
|
);
|
||||||
// Write key to file
|
// Write key to file
|
||||||
let mut f: File = match File::create(ssh_key_path.as_path()) {
|
let mut f: File = match File::create(ssh_key_path.as_path()) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(err) => return Self::make_io_err(err),
|
Err(err) => return Self::make_io_err(err),
|
||||||
};
|
};
|
||||||
if let Err(err) = f.write_all(ssh_key.as_bytes()) {
|
if let Err(err) = f.write_all(ssh_key.as_bytes()) {
|
||||||
|
error!("Failed to write SSH key to file: {}", err);
|
||||||
return Self::make_io_err(err);
|
return Self::make_io_err(err);
|
||||||
}
|
}
|
||||||
// Add host to keys
|
// Add host to keys
|
||||||
@@ -234,6 +268,7 @@ impl ConfigClient {
|
|||||||
/// and also commits changes to configuration, to prevent incoerent data
|
/// and also commits changes to configuration, to prevent incoerent data
|
||||||
pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> {
|
pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> {
|
||||||
// Remove key from configuration and get key path
|
// Remove key from configuration and get key path
|
||||||
|
info!("Removing key for {}@{}", host, username);
|
||||||
let key_path: PathBuf = match self
|
let key_path: PathBuf = match self
|
||||||
.config
|
.config
|
||||||
.remote
|
.remote
|
||||||
@@ -245,6 +280,7 @@ impl ConfigClient {
|
|||||||
};
|
};
|
||||||
// Remove file
|
// Remove file
|
||||||
if let Err(err) = remove_file(key_path.as_path()) {
|
if let Err(err) = remove_file(key_path.as_path()) {
|
||||||
|
error!("Failed to remove key file {}: {}", key_path.display(), err);
|
||||||
return Self::make_io_err(err);
|
return Self::make_io_err(err);
|
||||||
}
|
}
|
||||||
// Commit changes to configuration
|
// Commit changes to configuration
|
||||||
@@ -293,10 +329,13 @@ impl ConfigClient {
|
|||||||
let serializer: ConfigSerializer = ConfigSerializer {};
|
let serializer: ConfigSerializer = ConfigSerializer {};
|
||||||
serializer.serialize(Box::new(writer), &self.config)
|
serializer.serialize(Box::new(writer), &self.config)
|
||||||
}
|
}
|
||||||
Err(err) => Err(SerializerError::new_ex(
|
Err(err) => {
|
||||||
SerializerErrorKind::IoError,
|
error!("Failed to write configuration file: {}", err);
|
||||||
err.to_string(),
|
Err(SerializerError::new_ex(
|
||||||
)),
|
SerializerErrorKind::IoError,
|
||||||
|
err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,10 +359,13 @@ impl ConfigClient {
|
|||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => Err(SerializerError::new_ex(
|
Err(err) => {
|
||||||
SerializerErrorKind::IoError,
|
error!("Failed to read configuration: {}", err);
|
||||||
err.to_string(),
|
Err(SerializerError::new_ex(
|
||||||
)),
|
SerializerErrorKind::IoError,
|
||||||
|
err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +406,7 @@ mod tests {
|
|||||||
use crate::config::UserConfig;
|
use crate::config::UserConfig;
|
||||||
use crate::utils::random::random_alphanumeric_with_len;
|
use crate::utils::random::random_alphanumeric_with_len;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -496,18 +539,36 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_system_config_file_fmt() {
|
fn test_system_config_local_file_fmt() {
|
||||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||||
.ok()
|
.ok()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(client.get_file_fmt(), None);
|
assert_eq!(client.get_local_file_fmt(), None);
|
||||||
client.set_file_fmt(String::from("{NAME}"));
|
client.set_local_file_fmt(String::from("{NAME}"));
|
||||||
assert_eq!(client.get_file_fmt().unwrap(), String::from("{NAME}"));
|
assert_eq!(client.get_local_file_fmt().unwrap(), String::from("{NAME}"));
|
||||||
// Delete
|
// Delete
|
||||||
client.set_file_fmt(String::from(""));
|
client.set_local_file_fmt(String::from(""));
|
||||||
assert_eq!(client.get_file_fmt(), None);
|
assert_eq!(client.get_local_file_fmt(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_config_remote_file_fmt() {
|
||||||
|
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||||
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
|
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(client.get_remote_file_fmt(), None);
|
||||||
|
client.set_remote_file_fmt(String::from("{NAME}"));
|
||||||
|
assert_eq!(
|
||||||
|
client.get_remote_file_fmt().unwrap(),
|
||||||
|
String::from("{NAME}")
|
||||||
|
);
|
||||||
|
// Delete
|
||||||
|
client.set_remote_file_fmt(String::from(""));
|
||||||
|
assert_eq!(client.get_remote_file_fmt(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -87,11 +87,21 @@ pub fn get_config_paths(config_dir: &Path) -> (PathBuf, PathBuf) {
|
|||||||
(bookmarks_file, keys_dir)
|
(bookmarks_file, keys_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### get_log_paths
|
||||||
|
///
|
||||||
|
/// Returns the path for the supposed log file
|
||||||
|
pub fn get_log_paths(config_dir: &Path) -> PathBuf {
|
||||||
|
let mut log_file: PathBuf = PathBuf::from(config_dir);
|
||||||
|
log_file.push("termscp.log");
|
||||||
|
log_file
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
@@ -142,4 +152,12 @@ mod tests {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_environment_get_log_paths() {
|
||||||
|
assert_eq!(
|
||||||
|
get_log_paths(&Path::new("/home/omar/.config/termscp/")),
|
||||||
|
PathBuf::from("/home/omar/.config/termscp/termscp.log"),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_system_keys_filestorage_make_dir() {
|
fn test_system_keys_filestorage_make_dir() {
|
||||||
let storage: FileStorage = FileStorage::new(&Path::new("/tmp/"));
|
let storage: FileStorage = FileStorage::new(&Path::new("/tmp/"));
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ mod tests {
|
|||||||
extern crate whoami;
|
extern crate whoami;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use whoami::username;
|
use whoami::username;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_system_keys_mod_errors() {
|
fn test_system_keys_mod_errors() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
//! ## Logging
|
||||||
|
//!
|
||||||
|
//! `logging` is the module which initializes the logging system for termscp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// locals
|
||||||
|
use crate::system::environment::{get_log_paths, init_config_dir};
|
||||||
|
use crate::utils::file::open_file;
|
||||||
|
// ext
|
||||||
|
use simplelog::{ConfigBuilder, LevelFilter, WriteLogger};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// ### init
|
||||||
|
///
|
||||||
|
/// Initialize logger
|
||||||
|
pub fn init() -> Result<(), String> {
|
||||||
|
// Init config dir
|
||||||
|
let config_dir: PathBuf = match init_config_dir() {
|
||||||
|
Ok(Some(p)) => p,
|
||||||
|
Ok(None) => {
|
||||||
|
return Err(String::from(
|
||||||
|
"This system doesn't seem to support CONFIG_DIR",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
let log_file_path: PathBuf = get_log_paths(config_dir.as_path());
|
||||||
|
// Open log file
|
||||||
|
let file: File = open_file(log_file_path.as_path(), true, true, false)
|
||||||
|
.map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?;
|
||||||
|
// Prepare log config
|
||||||
|
let config = ConfigBuilder::new()
|
||||||
|
.set_time_format_str("%Y-%m-%dT%H:%M:%S%z")
|
||||||
|
.build();
|
||||||
|
// Make logger
|
||||||
|
WriteLogger::init(LevelFilter::Trace, config, file)
|
||||||
|
.map_err(|e| format!("Failed to initialize logger: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_logging_setup() {
|
||||||
|
assert!(init().is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,4 +30,5 @@ pub mod bookmarks_client;
|
|||||||
pub mod config_client;
|
pub mod config_client;
|
||||||
pub mod environment;
|
pub mod environment;
|
||||||
pub(crate) mod keys;
|
pub(crate) mod keys;
|
||||||
|
pub mod logging;
|
||||||
pub mod sshkey_storage;
|
pub mod sshkey_storage;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ impl SshKeyStorage {
|
|||||||
pub fn storage_from_config(cfg_client: &ConfigClient) -> Self {
|
pub fn storage_from_config(cfg_client: &ConfigClient) -> Self {
|
||||||
let mut hosts: HashMap<String, PathBuf> =
|
let mut hosts: HashMap<String, PathBuf> =
|
||||||
HashMap::with_capacity(cfg_client.iter_ssh_keys().count());
|
HashMap::with_capacity(cfg_client.iter_ssh_keys().count());
|
||||||
|
debug!("Setting up SSH key storage");
|
||||||
// Iterate over keys
|
// Iterate over keys
|
||||||
for key in cfg_client.iter_ssh_keys() {
|
for key in cfg_client.iter_ssh_keys() {
|
||||||
match cfg_client.get_ssh_key(key) {
|
match cfg_client.get_ssh_key(key) {
|
||||||
@@ -52,8 +53,12 @@ impl SshKeyStorage {
|
|||||||
}
|
}
|
||||||
None => continue,
|
None => continue,
|
||||||
},
|
},
|
||||||
Err(_) => continue,
|
Err(err) => {
|
||||||
|
error!("Failed to get SSH key for {}: {}", key, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
info!("Got SSH key for {}", key);
|
||||||
}
|
}
|
||||||
// Return storage
|
// Return storage
|
||||||
SshKeyStorage { hosts }
|
SshKeyStorage { hosts }
|
||||||
@@ -88,8 +93,9 @@ impl SshKeyStorage {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ extern crate dirs;
|
|||||||
use super::{AuthActivity, FileTransferProtocol};
|
use super::{AuthActivity, FileTransferProtocol};
|
||||||
use crate::system::bookmarks_client::BookmarksClient;
|
use crate::system::bookmarks_client::BookmarksClient;
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
use crate::ui::layout::props::PropValue;
|
|
||||||
use crate::ui::layout::Payload;
|
|
||||||
|
|
||||||
// Ext
|
// Ext
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use tuirealm::components::{input::InputPropsBuilder, radio::RadioPropsBuilder};
|
||||||
|
use tuirealm::{Payload, PropsBuilder, Value};
|
||||||
|
|
||||||
impl AuthActivity {
|
impl AuthActivity {
|
||||||
/// ### del_bookmark
|
/// ### del_bookmark
|
||||||
@@ -83,10 +83,10 @@ impl AuthActivity {
|
|||||||
let password: Option<String> = match save_password {
|
let password: Option<String> = match save_password {
|
||||||
true => match self
|
true => match self
|
||||||
.view
|
.view
|
||||||
.get_value(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
|
.get_state(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
|
||||||
{
|
{
|
||||||
Some(Payload::Unsigned(0)) => Some(password), // Yes
|
Some(Payload::One(Value::Usize(0))) => Some(password), // Yes
|
||||||
_ => None, // No such component / No
|
_ => None, // No such component / No
|
||||||
},
|
},
|
||||||
false => None,
|
false => None,
|
||||||
};
|
};
|
||||||
@@ -246,32 +246,29 @@ impl AuthActivity {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) {
|
) {
|
||||||
// Load parameters into components
|
// Load parameters into components
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
|
||||||
let props = props.with_value(PropValue::Str(addr)).build();
|
let props = InputPropsBuilder::from(props).with_value(addr).build();
|
||||||
self.view.update(super::COMPONENT_INPUT_ADDR, props);
|
self.view.update(super::COMPONENT_INPUT_ADDR, props);
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
|
||||||
let props = props.with_value(PropValue::Str(port.to_string())).build();
|
let props = InputPropsBuilder::from(props)
|
||||||
|
.with_value(port.to_string())
|
||||||
|
.build();
|
||||||
self.view.update(super::COMPONENT_INPUT_PORT, props);
|
self.view.update(super::COMPONENT_INPUT_PORT, props);
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
|
||||||
let props = props
|
let props = RadioPropsBuilder::from(props)
|
||||||
.with_value(PropValue::Unsigned(match protocol {
|
.with_value(Self::protocol_enum_to_opt(protocol))
|
||||||
FileTransferProtocol::Sftp => 0,
|
|
||||||
FileTransferProtocol::Scp => 1,
|
|
||||||
FileTransferProtocol::Ftp(false) => 2,
|
|
||||||
FileTransferProtocol::Ftp(true) => 3,
|
|
||||||
}))
|
|
||||||
.build();
|
.build();
|
||||||
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
|
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
|
||||||
let props = props.with_value(PropValue::Str(username)).build();
|
let props = InputPropsBuilder::from(props).with_value(username).build();
|
||||||
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
|
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
|
||||||
}
|
}
|
||||||
if let Some(password) = password {
|
if let Some(password) = password {
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
|
||||||
let props = props.with_value(PropValue::Str(password)).build();
|
let props = InputPropsBuilder::from(props).with_value(password).build();
|
||||||
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
|
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
//! ## AuthActivity
|
||||||
|
//!
|
||||||
|
//! `auth_activity` is the module which implements the authentication activity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
use super::{AuthActivity, FileTransferProtocol};
|
||||||
|
|
||||||
|
impl AuthActivity {
|
||||||
|
/// ### protocol_opt_to_enum
|
||||||
|
///
|
||||||
|
/// Convert radio index for protocol into a `FileTransferProtocol`
|
||||||
|
pub(super) fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol {
|
||||||
|
match protocol {
|
||||||
|
1 => FileTransferProtocol::Scp,
|
||||||
|
2 => FileTransferProtocol::Ftp(false),
|
||||||
|
3 => FileTransferProtocol::Ftp(true),
|
||||||
|
_ => FileTransferProtocol::Sftp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### protocol_enum_to_opt
|
||||||
|
///
|
||||||
|
/// Convert `FileTransferProtocol` enum into radio group index
|
||||||
|
pub(super) fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize {
|
||||||
|
match protocol {
|
||||||
|
FileTransferProtocol::Sftp => 0,
|
||||||
|
FileTransferProtocol::Scp => 1,
|
||||||
|
FileTransferProtocol::Ftp(false) => 2,
|
||||||
|
FileTransferProtocol::Ftp(true) => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_default_port_for_protocol
|
||||||
|
///
|
||||||
|
/// Get the default port for protocol
|
||||||
|
pub(super) fn get_default_port_for_protocol(protocol: FileTransferProtocol) -> u16 {
|
||||||
|
match protocol {
|
||||||
|
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
|
||||||
|
FileTransferProtocol::Ftp(_) => 21,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### is_port_standard
|
||||||
|
///
|
||||||
|
/// Returns whether the port is standard or not
|
||||||
|
pub(super) fn is_port_standard(port: u16) -> bool {
|
||||||
|
port < 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,26 +27,28 @@
|
|||||||
*/
|
*/
|
||||||
// Sub modules
|
// Sub modules
|
||||||
mod bookmarks;
|
mod bookmarks;
|
||||||
|
mod misc;
|
||||||
mod update;
|
mod update;
|
||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
// Dependencies
|
// Dependencies
|
||||||
extern crate crossterm;
|
extern crate crossterm;
|
||||||
extern crate tui;
|
extern crate tuirealm;
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use super::{Activity, Context, ExitReason};
|
use super::{Activity, Context, ExitReason};
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
use crate::system::bookmarks_client::BookmarksClient;
|
use crate::system::bookmarks_client::BookmarksClient;
|
||||||
use crate::ui::context::FileTransferParams;
|
use crate::ui::context::FileTransferParams;
|
||||||
use crate::ui::layout::view::View;
|
|
||||||
use crate::utils::git;
|
use crate::utils::git;
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||||
|
use tuirealm::View;
|
||||||
|
|
||||||
// -- components
|
// -- components
|
||||||
const COMPONENT_TEXT_HEADER: &str = "TEXT_HEADER";
|
const COMPONENT_TEXT_H1: &str = "TEXT_H1";
|
||||||
|
const COMPONENT_TEXT_H2: &str = "TEXT_H2";
|
||||||
const COMPONENT_TEXT_NEW_VERSION: &str = "TEXT_NEW_VERSION";
|
const COMPONENT_TEXT_NEW_VERSION: &str = "TEXT_NEW_VERSION";
|
||||||
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
|
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
|
||||||
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
||||||
@@ -106,17 +108,24 @@ impl AuthActivity {
|
|||||||
///
|
///
|
||||||
/// If enabled in configuration, check for updates from Github
|
/// If enabled in configuration, check for updates from Github
|
||||||
fn check_for_updates(&mut self) {
|
fn check_for_updates(&mut self) {
|
||||||
|
debug!("Check for updates...");
|
||||||
// Check version only if unset in the store
|
// Check version only if unset in the store
|
||||||
let ctx: &Context = self.context.as_ref().unwrap();
|
let ctx: &Context = self.context.as_ref().unwrap();
|
||||||
if !ctx.store.isset(STORE_KEY_LATEST_VERSION) {
|
if !ctx.store.isset(STORE_KEY_LATEST_VERSION) {
|
||||||
|
debug!("Version is not set in storage");
|
||||||
let mut new_version: Option<String> = match ctx.config_client.as_ref() {
|
let mut new_version: Option<String> = match ctx.config_client.as_ref() {
|
||||||
Some(client) => {
|
Some(client) => {
|
||||||
if client.get_check_for_updates() {
|
if client.get_check_for_updates() {
|
||||||
|
debug!("Check for updates is enabled");
|
||||||
// Send request
|
// Send request
|
||||||
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
|
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
|
||||||
Ok(version) => version,
|
Ok(version) => {
|
||||||
|
info!("Latest version is: {:?}", version);
|
||||||
|
version
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Report error
|
// Report error
|
||||||
|
error!("Failed to get latest version: {}", err);
|
||||||
self.mount_error(
|
self.mount_error(
|
||||||
format!("Could not check for new updates: {}", err).as_str(),
|
format!("Could not check for new updates: {}", err).as_str(),
|
||||||
);
|
);
|
||||||
@@ -125,6 +134,7 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
info!("Check for updates is disabled");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +157,7 @@ impl Activity for AuthActivity {
|
|||||||
/// `on_create` must initialize all the data structures used by the activity
|
/// `on_create` must initialize all the data structures used by the activity
|
||||||
/// Context is taken from activity manager and will be released only when activity is destroyed
|
/// Context is taken from activity manager and will be released only when activity is destroyed
|
||||||
fn on_create(&mut self, mut context: Context) {
|
fn on_create(&mut self, mut context: Context) {
|
||||||
|
debug!("Initializing activity");
|
||||||
// Initialize file transfer params
|
// Initialize file transfer params
|
||||||
context.ft_params = Some(FileTransferParams::default());
|
context.ft_params = Some(FileTransferParams::default());
|
||||||
// Set context
|
// Set context
|
||||||
@@ -154,19 +165,25 @@ impl Activity for AuthActivity {
|
|||||||
// Clear terminal
|
// Clear terminal
|
||||||
self.context.as_mut().unwrap().clear_screen();
|
self.context.as_mut().unwrap().clear_screen();
|
||||||
// Put raw mode on enabled
|
// Put raw mode on enabled
|
||||||
let _ = enable_raw_mode();
|
if let Err(err) = enable_raw_mode() {
|
||||||
// Init bookmarks client
|
error!("Failed to enter raw mode: {}", err);
|
||||||
if self.bookmarks_client.is_none() {
|
|
||||||
self.init_bookmarks_client();
|
|
||||||
}
|
|
||||||
// Verify error state from context
|
|
||||||
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
|
||||||
self.mount_error(err.as_str());
|
|
||||||
}
|
}
|
||||||
// If check for updates is enabled, check for updates
|
// If check for updates is enabled, check for updates
|
||||||
self.check_for_updates();
|
self.check_for_updates();
|
||||||
// Initialize view
|
// Initialize view
|
||||||
self.init();
|
self.init();
|
||||||
|
// Init bookmarks client
|
||||||
|
if self.bookmarks_client.is_none() {
|
||||||
|
self.init_bookmarks_client();
|
||||||
|
// View bookarmsk
|
||||||
|
self.view_bookmarks();
|
||||||
|
self.view_recent_connections();
|
||||||
|
}
|
||||||
|
// Verify error state from context
|
||||||
|
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||||
|
self.mount_error(err.as_str());
|
||||||
|
}
|
||||||
|
info!("Activity initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### on_draw
|
/// ### on_draw
|
||||||
@@ -211,7 +228,9 @@ impl Activity for AuthActivity {
|
|||||||
/// This function finally releases the context
|
/// This function finally releases the context
|
||||||
fn on_destroy(&mut self) -> Option<Context> {
|
fn on_destroy(&mut self) -> Option<Context> {
|
||||||
// Disable raw mode
|
// Disable raw mode
|
||||||
let _ = disable_raw_mode();
|
if let Err(err) = disable_raw_mode() {
|
||||||
|
error!("Failed to disable raw mode: {}", err);
|
||||||
|
}
|
||||||
self.context.as_ref()?;
|
self.context.as_ref()?;
|
||||||
// Clear terminal and return
|
// Clear terminal and return
|
||||||
match self.context.take() {
|
match self.context.take() {
|
||||||
@@ -27,15 +27,16 @@
|
|||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{
|
use super::{
|
||||||
AuthActivity, FileTransferParams, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
|
AuthActivity, FileTransferParams, FileTransferProtocol, COMPONENT_BOOKMARKS_LIST,
|
||||||
COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT,
|
COMPONENT_INPUT_ADDR, COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD,
|
||||||
COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
COMPONENT_INPUT_PORT, COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||||
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
|
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
|
||||||
COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR,
|
COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR,
|
||||||
COMPONENT_TEXT_HELP,
|
COMPONENT_TEXT_HELP,
|
||||||
};
|
};
|
||||||
use crate::ui::activities::keymap::*;
|
use crate::ui::keymap::*;
|
||||||
use crate::ui::layout::{Msg, Payload};
|
use tuirealm::components::InputPropsBuilder;
|
||||||
|
use tuirealm::{Msg, Payload, PropsBuilder, Value};
|
||||||
|
|
||||||
// -- update
|
// -- update
|
||||||
|
|
||||||
@@ -51,17 +52,17 @@ impl AuthActivity {
|
|||||||
None => None, // Exit after None
|
None => None, // Exit after None
|
||||||
Some(msg) => match msg {
|
Some(msg) => match msg {
|
||||||
// Focus ( DOWN )
|
// Focus ( DOWN )
|
||||||
|
(COMPONENT_RADIO_PROTOCOL, &MSG_KEY_DOWN) => {
|
||||||
|
// Give focus to port
|
||||||
|
self.view.active(COMPONENT_INPUT_ADDR);
|
||||||
|
None
|
||||||
|
}
|
||||||
(COMPONENT_INPUT_ADDR, &MSG_KEY_DOWN) => {
|
(COMPONENT_INPUT_ADDR, &MSG_KEY_DOWN) => {
|
||||||
// Give focus to port
|
// Give focus to port
|
||||||
self.view.active(COMPONENT_INPUT_PORT);
|
self.view.active(COMPONENT_INPUT_PORT);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_PORT, &MSG_KEY_DOWN) => {
|
(COMPONENT_INPUT_PORT, &MSG_KEY_DOWN) => {
|
||||||
// Give focus to port
|
|
||||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
(COMPONENT_RADIO_PROTOCOL, &MSG_KEY_DOWN) => {
|
|
||||||
// Give focus to port
|
// Give focus to port
|
||||||
self.view.active(COMPONENT_INPUT_USERNAME);
|
self.view.active(COMPONENT_INPUT_USERNAME);
|
||||||
None
|
None
|
||||||
@@ -73,7 +74,7 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
(COMPONENT_INPUT_PASSWORD, &MSG_KEY_DOWN) => {
|
(COMPONENT_INPUT_PASSWORD, &MSG_KEY_DOWN) => {
|
||||||
// Give focus to port
|
// Give focus to port
|
||||||
self.view.active(COMPONENT_INPUT_ADDR);
|
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
// Focus ( UP )
|
// Focus ( UP )
|
||||||
@@ -83,11 +84,6 @@ impl AuthActivity {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_USERNAME, &MSG_KEY_UP) => {
|
(COMPONENT_INPUT_USERNAME, &MSG_KEY_UP) => {
|
||||||
// Give focus to port
|
|
||||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
(COMPONENT_RADIO_PROTOCOL, &MSG_KEY_UP) => {
|
|
||||||
// Give focus to port
|
// Give focus to port
|
||||||
self.view.active(COMPONENT_INPUT_PORT);
|
self.view.active(COMPONENT_INPUT_PORT);
|
||||||
None
|
None
|
||||||
@@ -98,10 +94,28 @@ impl AuthActivity {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_ADDR, &MSG_KEY_UP) => {
|
(COMPONENT_INPUT_ADDR, &MSG_KEY_UP) => {
|
||||||
|
// Give focus to port
|
||||||
|
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
(COMPONENT_RADIO_PROTOCOL, &MSG_KEY_UP) => {
|
||||||
// Give focus to port
|
// Give focus to port
|
||||||
self.view.active(COMPONENT_INPUT_PASSWORD);
|
self.view.active(COMPONENT_INPUT_PASSWORD);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
// Protocol - On Change
|
||||||
|
(COMPONENT_RADIO_PROTOCOL, Msg::OnChange(Payload::One(Value::Usize(protocol)))) => {
|
||||||
|
// If port is standard, update the current port with default for selected protocol
|
||||||
|
let protocol: FileTransferProtocol = Self::protocol_opt_to_enum(*protocol);
|
||||||
|
// Get port
|
||||||
|
let port: u16 = self.get_input_port();
|
||||||
|
match Self::is_port_standard(port) {
|
||||||
|
false => None, // Return None
|
||||||
|
true => {
|
||||||
|
self.update_input_port(Self::get_default_port_for_protocol(protocol))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// <TAB> bookmarks
|
// <TAB> bookmarks
|
||||||
(COMPONENT_BOOKMARKS_LIST, &MSG_KEY_TAB)
|
(COMPONENT_BOOKMARKS_LIST, &MSG_KEY_TAB)
|
||||||
| (COMPONENT_RECENTS_LIST, &MSG_KEY_TAB) => {
|
| (COMPONENT_RECENTS_LIST, &MSG_KEY_TAB) => {
|
||||||
@@ -140,13 +154,13 @@ impl AuthActivity {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
// Enter
|
// Enter
|
||||||
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::Unsigned(idx))) => {
|
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
|
||||||
self.load_bookmark(*idx);
|
self.load_bookmark(*idx);
|
||||||
// Give focus to input password
|
// Give focus to input password
|
||||||
self.view.active(COMPONENT_INPUT_PASSWORD);
|
self.view.active(COMPONENT_INPUT_PASSWORD);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::Unsigned(idx))) => {
|
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
|
||||||
self.load_recent(*idx);
|
self.load_recent(*idx);
|
||||||
// Give focus to input password
|
// Give focus to input password
|
||||||
self.view.active(COMPONENT_INPUT_PASSWORD);
|
self.view.active(COMPONENT_INPUT_PASSWORD);
|
||||||
@@ -156,7 +170,7 @@ impl AuthActivity {
|
|||||||
// Del bookmarks
|
// Del bookmarks
|
||||||
(
|
(
|
||||||
COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||||
Msg::OnSubmit(Payload::Unsigned(index)),
|
Msg::OnSubmit(Payload::One(Value::Usize(index))),
|
||||||
) => {
|
) => {
|
||||||
// hide bookmark delete
|
// hide bookmark delete
|
||||||
self.umount_bookmark_del_dialog();
|
self.umount_bookmark_del_dialog();
|
||||||
@@ -164,8 +178,8 @@ impl AuthActivity {
|
|||||||
match *index {
|
match *index {
|
||||||
0 => {
|
0 => {
|
||||||
// Get selected bookmark
|
// Get selected bookmark
|
||||||
match self.view.get_value(COMPONENT_BOOKMARKS_LIST) {
|
match self.view.get_state(COMPONENT_BOOKMARKS_LIST) {
|
||||||
Some(Payload::Unsigned(index)) => {
|
Some(Payload::One(Value::Usize(index))) => {
|
||||||
// Delete bookmark
|
// Delete bookmark
|
||||||
self.del_bookmark(index);
|
self.del_bookmark(index);
|
||||||
// Update bookmarks
|
// Update bookmarks
|
||||||
@@ -177,15 +191,18 @@ impl AuthActivity {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, Msg::OnSubmit(Payload::Unsigned(index))) => {
|
(
|
||||||
|
COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
|
||||||
|
Msg::OnSubmit(Payload::One(Value::Usize(index))),
|
||||||
|
) => {
|
||||||
// hide bookmark delete
|
// hide bookmark delete
|
||||||
self.umount_recent_del_dialog();
|
self.umount_recent_del_dialog();
|
||||||
// Index must be 0 => YES
|
// Index must be 0 => YES
|
||||||
match *index {
|
match *index {
|
||||||
0 => {
|
0 => {
|
||||||
// Get selected bookmark
|
// Get selected bookmark
|
||||||
match self.view.get_value(COMPONENT_RECENTS_LIST) {
|
match self.view.get_state(COMPONENT_RECENTS_LIST) {
|
||||||
Some(Payload::Unsigned(index)) => {
|
Some(Payload::One(Value::Usize(index))) => {
|
||||||
// Delete recent
|
// Delete recent
|
||||||
self.del_recent(index);
|
self.del_recent(index);
|
||||||
// Update bookmarks
|
// Update bookmarks
|
||||||
@@ -199,37 +216,20 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
// <ESC> hide tab
|
// <ESC> hide tab
|
||||||
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, &MSG_KEY_ESC) => {
|
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, &MSG_KEY_ESC) => {
|
||||||
match self
|
self.umount_recent_del_dialog();
|
||||||
.view
|
None
|
||||||
.get_props(COMPONENT_RADIO_BOOKMARK_DEL_RECENT)
|
|
||||||
.as_mut()
|
|
||||||
{
|
|
||||||
Some(props) => {
|
|
||||||
let msg = self.view.update(
|
|
||||||
COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
|
|
||||||
props.hidden().build(),
|
|
||||||
);
|
|
||||||
self.update(msg)
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, &MSG_KEY_ESC) => {
|
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, &MSG_KEY_ESC) => {
|
||||||
match self
|
self.umount_bookmark_del_dialog();
|
||||||
.view
|
None
|
||||||
.get_props(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK)
|
|
||||||
.as_mut()
|
|
||||||
{
|
|
||||||
Some(props) => {
|
|
||||||
let msg = self.view.update(
|
|
||||||
COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
|
||||||
props.hidden().build(),
|
|
||||||
);
|
|
||||||
self.update(msg)
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Error message
|
||||||
|
(COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) => {
|
||||||
|
// Umount text error
|
||||||
|
self.umount_error();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
(COMPONENT_TEXT_ERROR, _) => None,
|
||||||
// Help
|
// Help
|
||||||
(_, &MSG_KEY_CTRL_H) => {
|
(_, &MSG_KEY_CTRL_H) => {
|
||||||
// Show help
|
// Show help
|
||||||
@@ -269,16 +269,18 @@ impl AuthActivity {
|
|||||||
| (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, Msg::OnSubmit(_)) => {
|
| (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, Msg::OnSubmit(_)) => {
|
||||||
// Get values
|
// Get values
|
||||||
let bookmark_name: String =
|
let bookmark_name: String =
|
||||||
match self.view.get_value(COMPONENT_INPUT_BOOKMARK_NAME) {
|
match self.view.get_state(COMPONENT_INPUT_BOOKMARK_NAME) {
|
||||||
Some(Payload::Text(s)) => s,
|
Some(Payload::One(Value::Str(s))) => s,
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
};
|
};
|
||||||
let save_pwd: bool = matches!(
|
let save_pwd: bool = matches!(
|
||||||
self.view.get_value(COMPONENT_RADIO_BOOKMARK_SAVE_PWD),
|
self.view.get_state(COMPONENT_RADIO_BOOKMARK_SAVE_PWD),
|
||||||
Some(Payload::Unsigned(0))
|
Some(Payload::One(Value::Usize(0)))
|
||||||
);
|
);
|
||||||
// Save bookmark
|
// Save bookmark
|
||||||
self.save_bookmark(bookmark_name, save_pwd);
|
if !bookmark_name.is_empty() {
|
||||||
|
self.save_bookmark(bookmark_name, save_pwd);
|
||||||
|
}
|
||||||
// Umount popup
|
// Umount popup
|
||||||
self.umount_bookmark_save_dialog();
|
self.umount_bookmark_save_dialog();
|
||||||
// Reload bookmarks
|
// Reload bookmarks
|
||||||
@@ -291,14 +293,8 @@ impl AuthActivity {
|
|||||||
self.umount_bookmark_save_dialog();
|
self.umount_bookmark_save_dialog();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
// Error message
|
|
||||||
(COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) => {
|
|
||||||
// Umount text error
|
|
||||||
self.umount_error();
|
|
||||||
None
|
|
||||||
}
|
|
||||||
// Quit dialog
|
// Quit dialog
|
||||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(choice))) => {
|
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(choice)))) => {
|
||||||
// If choice is 0, quit termscp
|
// If choice is 0, quit termscp
|
||||||
if *choice == 0 {
|
if *choice == 0 {
|
||||||
self.exit_reason = Some(super::ExitReason::Quit);
|
self.exit_reason = Some(super::ExitReason::Quit);
|
||||||
@@ -343,4 +339,16 @@ impl AuthActivity {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_input_port(&mut self, port: u16) -> Option<(String, Msg)> {
|
||||||
|
match self.view.get_props(COMPONENT_INPUT_PORT) {
|
||||||
|
None => None,
|
||||||
|
Some(props) => {
|
||||||
|
let props = InputPropsBuilder::from(props)
|
||||||
|
.with_value(port.to_string())
|
||||||
|
.build();
|
||||||
|
self.view.update(COMPONENT_INPUT_PORT, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,20 +27,27 @@
|
|||||||
*/
|
*/
|
||||||
// Locals
|
// Locals
|
||||||
use super::{AuthActivity, Context, FileTransferProtocol};
|
use super::{AuthActivity, Context, FileTransferProtocol};
|
||||||
use crate::ui::layout::components::{
|
use crate::ui::components::{
|
||||||
bookmark_list::BookmarkList, input::Input, msgbox::MsgBox, radio_group::RadioGroup,
|
bookmark_list::{BookmarkList, BookmarkListPropsBuilder},
|
||||||
table::Table, text::Text, title::Title,
|
msgbox::{MsgBox, MsgBoxPropsBuilder},
|
||||||
};
|
};
|
||||||
use crate::ui::layout::props::{
|
use crate::utils::ui::draw_area_in;
|
||||||
InputType, PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
|
|
||||||
};
|
|
||||||
use crate::ui::layout::utils::draw_area_in;
|
|
||||||
use crate::ui::layout::{Msg, Payload};
|
|
||||||
// Ext
|
// Ext
|
||||||
use tui::{
|
use tuirealm::components::{
|
||||||
|
input::{Input, InputPropsBuilder},
|
||||||
|
label::{Label, LabelPropsBuilder},
|
||||||
|
radio::{Radio, RadioPropsBuilder},
|
||||||
|
span::{Span, SpanPropsBuilder},
|
||||||
|
table::{Table, TablePropsBuilder},
|
||||||
|
};
|
||||||
|
use tuirealm::tui::{
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
style::Color,
|
style::Color,
|
||||||
widgets::Clear,
|
widgets::{BorderType, Borders, Clear},
|
||||||
|
};
|
||||||
|
use tuirealm::{
|
||||||
|
props::{InputType, PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
|
||||||
|
Msg, Payload, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl AuthActivity {
|
impl AuthActivity {
|
||||||
@@ -48,37 +55,74 @@ impl AuthActivity {
|
|||||||
///
|
///
|
||||||
/// Initialize view, mounting all startup components inside the view
|
/// Initialize view, mounting all startup components inside the view
|
||||||
pub(super) fn init(&mut self) {
|
pub(super) fn init(&mut self) {
|
||||||
// Header
|
// Headers
|
||||||
self.view.mount(super::COMPONENT_TEXT_HEADER, Box::new(
|
self.view.mount(
|
||||||
Title::new(
|
super::COMPONENT_TEXT_H1,
|
||||||
PropsBuilder::default().with_foreground(Color::White).with_texts(
|
Box::new(Label::new(
|
||||||
TextParts::new(Some(String::from(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")), None)
|
LabelPropsBuilder::default()
|
||||||
).bold().build()
|
.bold()
|
||||||
)
|
.italic()
|
||||||
));
|
.with_text(String::from("$ termscp"))
|
||||||
|
.build(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
self.view.mount(
|
||||||
|
super::COMPONENT_TEXT_H2,
|
||||||
|
Box::new(Label::new(
|
||||||
|
LabelPropsBuilder::default()
|
||||||
|
.bold()
|
||||||
|
.italic()
|
||||||
|
.with_text(format!("$ version {}", env!("CARGO_PKG_VERSION")))
|
||||||
|
.build(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
// Footer
|
// Footer
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_FOOTER,
|
super::COMPONENT_TEXT_FOOTER,
|
||||||
Box::new(Text::new(
|
Box::new(Span::new(
|
||||||
PropsBuilder::default()
|
SpanPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.with_spans(vec![
|
||||||
None,
|
TextSpanBuilder::new("Press ").bold().build(),
|
||||||
Some(vec![
|
TextSpanBuilder::new("<CTRL+H>")
|
||||||
TextSpanBuilder::new("Press ").bold().build(),
|
.bold()
|
||||||
TextSpanBuilder::new("<CTRL+H>")
|
.with_foreground(Color::Cyan)
|
||||||
.bold()
|
.build(),
|
||||||
.with_foreground(Color::Cyan)
|
TextSpanBuilder::new(" to show keybindings; ")
|
||||||
.build(),
|
.bold()
|
||||||
TextSpanBuilder::new(" to show keybindings; ")
|
.build(),
|
||||||
.bold()
|
TextSpanBuilder::new("<CTRL+C>")
|
||||||
.build(),
|
.bold()
|
||||||
TextSpanBuilder::new("<CTRL+C>")
|
.with_foreground(Color::Cyan)
|
||||||
.bold()
|
.build(),
|
||||||
.with_foreground(Color::Cyan)
|
TextSpanBuilder::new(" to enter setup").bold().build(),
|
||||||
.build(),
|
])
|
||||||
TextSpanBuilder::new(" to enter setup").bold().build(),
|
.build(),
|
||||||
]),
|
)),
|
||||||
))
|
);
|
||||||
|
// Get default protocol
|
||||||
|
let default_protocol: FileTransferProtocol =
|
||||||
|
match self.context.as_ref().unwrap().config_client.as_ref() {
|
||||||
|
Some(cli) => cli.get_default_protocol(),
|
||||||
|
None => FileTransferProtocol::Sftp,
|
||||||
|
};
|
||||||
|
// Protocol
|
||||||
|
self.view.mount(
|
||||||
|
super::COMPONENT_RADIO_PROTOCOL,
|
||||||
|
Box::new(Radio::new(
|
||||||
|
RadioPropsBuilder::default()
|
||||||
|
.with_color(Color::LightGreen)
|
||||||
|
.with_inverted_color(Color::Black)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||||
|
.with_options(
|
||||||
|
Some(String::from("Protocol")),
|
||||||
|
vec![
|
||||||
|
String::from("SFTP"),
|
||||||
|
String::from("SCP"),
|
||||||
|
String::from("FTP"),
|
||||||
|
String::from("FTPS"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.with_value(Self::protocol_enum_to_opt(default_protocol))
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -86,9 +130,10 @@ impl AuthActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_ADDR,
|
super::COMPONENT_INPUT_ADDR,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_foreground(Color::Yellow)
|
.with_foreground(Color::Yellow)
|
||||||
.with_texts(TextParts::new(Some(String::from("Remote address")), None))
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||||
|
.with_label(String::from("Remote address"))
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -96,31 +141,13 @@ impl AuthActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_PORT,
|
super::COMPONENT_INPUT_PORT,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_foreground(Color::LightCyan)
|
.with_foreground(Color::LightCyan)
|
||||||
.with_texts(TextParts::new(Some(String::from("Port number")), None))
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
|
||||||
|
.with_label(String::from("Port number"))
|
||||||
.with_input(InputType::Number)
|
.with_input(InputType::Number)
|
||||||
.with_input_len(5)
|
.with_input_len(5)
|
||||||
.with_value(PropValue::Str(String::from("22")))
|
.with_value(Self::get_default_port_for_protocol(default_protocol).to_string())
|
||||||
.build(),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
// Protocol
|
|
||||||
self.view.mount(
|
|
||||||
super::COMPONENT_RADIO_PROTOCOL,
|
|
||||||
Box::new(RadioGroup::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_foreground(Color::LightGreen)
|
|
||||||
.with_background(Color::Black)
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(String::from("Protocol")),
|
|
||||||
Some(vec![
|
|
||||||
TextSpan::from("SFTP"),
|
|
||||||
TextSpan::from("SCP"),
|
|
||||||
TextSpan::from("FTP"),
|
|
||||||
TextSpan::from("FTPS"),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -128,9 +155,10 @@ impl AuthActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_USERNAME,
|
super::COMPONENT_INPUT_USERNAME,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_foreground(Color::LightMagenta)
|
.with_foreground(Color::LightMagenta)
|
||||||
.with_texts(TextParts::new(Some(String::from("Username")), None))
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
|
||||||
|
.with_label(String::from("Username"))
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -138,9 +166,10 @@ impl AuthActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_PASSWORD,
|
super::COMPONENT_INPUT_PASSWORD,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_foreground(Color::LightBlue)
|
.with_foreground(Color::LightBlue)
|
||||||
.with_texts(TextParts::new(Some(String::from("Password")), None))
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
|
||||||
|
.with_label(String::from("Password"))
|
||||||
.with_input(InputType::Password)
|
.with_input(InputType::Password)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
@@ -155,10 +184,16 @@ impl AuthActivity {
|
|||||||
{
|
{
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_NEW_VERSION,
|
super::COMPONENT_TEXT_NEW_VERSION,
|
||||||
Box::new(Text::new(
|
Box::new(Span::new(
|
||||||
PropsBuilder::default()
|
SpanPropsBuilder::default()
|
||||||
.with_foreground(Color::Yellow)
|
.with_foreground(Color::Yellow)
|
||||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(format!("TermSCP {} is now available! Download it from <https://github.com/veeso/termscp/releases/latest>", version))])))
|
.with_spans(
|
||||||
|
vec![
|
||||||
|
TextSpan::from("termscp "),
|
||||||
|
TextSpanBuilder::new(version).underlined().bold().build(),
|
||||||
|
TextSpan::from(" is now available! Download it from <https://github.com/veeso/termscp/releases/latest>")
|
||||||
|
]
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
@@ -167,10 +202,11 @@ impl AuthActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_BOOKMARKS_LIST,
|
super::COMPONENT_BOOKMARKS_LIST,
|
||||||
Box::new(BookmarkList::new(
|
Box::new(BookmarkList::new(
|
||||||
PropsBuilder::default()
|
BookmarkListPropsBuilder::default()
|
||||||
.with_background(Color::LightGreen)
|
.with_background(Color::LightGreen)
|
||||||
.with_foreground(Color::Black)
|
.with_foreground(Color::Black)
|
||||||
.with_texts(TextParts::new(Some(String::from("Bookmarks")), None))
|
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||||
|
.with_bookmarks(Some(String::from("Bookmarks")), vec![])
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -179,19 +215,17 @@ impl AuthActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RECENTS_LIST,
|
super::COMPONENT_RECENTS_LIST,
|
||||||
Box::new(BookmarkList::new(
|
Box::new(BookmarkList::new(
|
||||||
PropsBuilder::default()
|
BookmarkListPropsBuilder::default()
|
||||||
.with_background(Color::LightBlue)
|
.with_background(Color::LightBlue)
|
||||||
.with_foreground(Color::Black)
|
.with_foreground(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Plain, Color::LightBlue)
|
||||||
Some(String::from("Recent connections")),
|
.with_bookmarks(Some(String::from("Recent connections")), vec![])
|
||||||
None,
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
let _ = self.view_recent_connections();
|
let _ = self.view_recent_connections();
|
||||||
// Active address
|
// Active protocol
|
||||||
self.view.active(super::COMPONENT_INPUT_ADDR);
|
self.view.active(super::COMPONENT_RADIO_PROTOCOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### view
|
/// ### view
|
||||||
@@ -216,11 +250,12 @@ impl AuthActivity {
|
|||||||
let auth_chunks = Layout::default()
|
let auth_chunks = Layout::default()
|
||||||
.constraints(
|
.constraints(
|
||||||
[
|
[
|
||||||
Constraint::Length(6), // header
|
Constraint::Length(1), // h1
|
||||||
|
Constraint::Length(1), // h2
|
||||||
Constraint::Length(1), // Version
|
Constraint::Length(1), // Version
|
||||||
|
Constraint::Length(3), // protocol
|
||||||
Constraint::Length(3), // host
|
Constraint::Length(3), // host
|
||||||
Constraint::Length(3), // port
|
Constraint::Length(3), // port
|
||||||
Constraint::Length(3), // protocol
|
|
||||||
Constraint::Length(3), // username
|
Constraint::Length(3), // username
|
||||||
Constraint::Length(3), // password
|
Constraint::Length(3), // password
|
||||||
Constraint::Length(3), // footer
|
Constraint::Length(3), // footer
|
||||||
@@ -237,48 +272,50 @@ impl AuthActivity {
|
|||||||
// Render
|
// Render
|
||||||
// Auth chunks
|
// Auth chunks
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_TEXT_HEADER, f, auth_chunks[0]);
|
.render(super::COMPONENT_TEXT_H1, f, auth_chunks[0]);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_TEXT_NEW_VERSION, f, auth_chunks[1]);
|
.render(super::COMPONENT_TEXT_H2, f, auth_chunks[1]);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_INPUT_ADDR, f, auth_chunks[2]);
|
.render(super::COMPONENT_TEXT_NEW_VERSION, f, auth_chunks[2]);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_INPUT_PORT, f, auth_chunks[3]);
|
.render(super::COMPONENT_RADIO_PROTOCOL, f, auth_chunks[3]);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_RADIO_PROTOCOL, f, auth_chunks[4]);
|
.render(super::COMPONENT_INPUT_ADDR, f, auth_chunks[4]);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_INPUT_USERNAME, f, auth_chunks[5]);
|
.render(super::COMPONENT_INPUT_PORT, f, auth_chunks[5]);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_INPUT_PASSWORD, f, auth_chunks[6]);
|
.render(super::COMPONENT_INPUT_USERNAME, f, auth_chunks[6]);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[7]);
|
.render(super::COMPONENT_INPUT_PASSWORD, f, auth_chunks[7]);
|
||||||
|
self.view
|
||||||
|
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[8]);
|
||||||
// Bookmark chunks
|
// Bookmark chunks
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]);
|
.render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_RECENTS_LIST, f, bookmark_chunks[1]);
|
.render(super::COMPONENT_RECENTS_LIST, f, bookmark_chunks[1]);
|
||||||
// Popups
|
// Popups
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 50, 10);
|
let popup = draw_area_in(f.size(), 50, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
// make popup
|
// make popup
|
||||||
let popup = draw_area_in(f.size(), 30, 10);
|
let popup = draw_area_in(f.size(), 30, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self
|
if let Some(props) = self
|
||||||
.view
|
.view
|
||||||
.get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK)
|
.get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK)
|
||||||
{
|
{
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
// make popup
|
// make popup
|
||||||
let popup = draw_area_in(f.size(), 30, 10);
|
let popup = draw_area_in(f.size(), 30, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
@@ -286,11 +323,11 @@ impl AuthActivity {
|
|||||||
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, f, popup);
|
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self
|
if let Some(props) = self
|
||||||
.view
|
.view
|
||||||
.get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT)
|
.get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT)
|
||||||
{
|
{
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
// make popup
|
// make popup
|
||||||
let popup = draw_area_in(f.size(), 30, 10);
|
let popup = draw_area_in(f.size(), 30, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
@@ -298,19 +335,19 @@ impl AuthActivity {
|
|||||||
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT, f, popup);
|
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
// make popup
|
// make popup
|
||||||
let popup = draw_area_in(f.size(), 50, 70);
|
let popup = draw_area_in(f.size(), 50, 70);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self
|
if let Some(props) = self
|
||||||
.view
|
.view
|
||||||
.get_props(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
|
.get_props(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
|
||||||
{
|
{
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
// make popup
|
// make popup
|
||||||
let popup = draw_area_in(f.size(), 20, 20);
|
let popup = draw_area_in(f.size(), 20, 20);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
@@ -340,7 +377,7 @@ impl AuthActivity {
|
|||||||
///
|
///
|
||||||
/// Make text span from bookmarks
|
/// Make text span from bookmarks
|
||||||
pub(super) fn view_bookmarks(&mut self) -> Option<(String, Msg)> {
|
pub(super) fn view_bookmarks(&mut self) -> Option<(String, Msg)> {
|
||||||
let bookmarks: Vec<TextSpan> = self
|
let bookmarks: Vec<String> = self
|
||||||
.bookmarks_list
|
.bookmarks_list
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
@@ -350,33 +387,23 @@ impl AuthActivity {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.get_bookmark(x)
|
.get_bookmark(x)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
TextSpan::from(
|
format!(
|
||||||
format!(
|
"{} ({}://{}@{}:{})",
|
||||||
"{} ({}://{}@{}:{})",
|
x,
|
||||||
x,
|
entry.2.to_string().to_lowercase(),
|
||||||
entry.2.to_string().to_lowercase(),
|
entry.3,
|
||||||
entry.3,
|
entry.0,
|
||||||
entry.0,
|
entry.1
|
||||||
entry.1
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
match self
|
match self.view.get_props(super::COMPONENT_BOOKMARKS_LIST) {
|
||||||
.view
|
|
||||||
.get_props(super::COMPONENT_BOOKMARKS_LIST)
|
|
||||||
.as_mut()
|
|
||||||
{
|
|
||||||
None => None,
|
None => None,
|
||||||
Some(props) => {
|
Some(props) => {
|
||||||
let msg = self.view.update(
|
let msg = self.view.update(
|
||||||
super::COMPONENT_BOOKMARKS_LIST,
|
super::COMPONENT_BOOKMARKS_LIST,
|
||||||
props
|
BookmarkListPropsBuilder::from(props)
|
||||||
.with_texts(TextParts::new(
|
.with_bookmarks(Some(String::from("Bookmarks")), bookmarks)
|
||||||
Some(String::from("Bookmarks")),
|
|
||||||
Some(bookmarks),
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
msg
|
msg
|
||||||
@@ -388,7 +415,7 @@ impl AuthActivity {
|
|||||||
///
|
///
|
||||||
/// View recent connections
|
/// View recent connections
|
||||||
pub(super) fn view_recent_connections(&mut self) -> Option<(String, Msg)> {
|
pub(super) fn view_recent_connections(&mut self) -> Option<(String, Msg)> {
|
||||||
let bookmarks: Vec<TextSpan> = self
|
let bookmarks: Vec<String> = self
|
||||||
.recents_list
|
.recents_list
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
@@ -398,28 +425,23 @@ impl AuthActivity {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.get_recent(x)
|
.get_recent(x)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
TextSpan::from(
|
|
||||||
format!(
|
format!(
|
||||||
"{}://{}@{}:{}",
|
"{}://{}@{}:{}",
|
||||||
entry.2.to_string().to_lowercase(),
|
entry.2.to_string().to_lowercase(),
|
||||||
entry.3,
|
entry.3,
|
||||||
entry.0,
|
entry.0,
|
||||||
entry.1
|
entry.1
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
match self.view.get_props(super::COMPONENT_RECENTS_LIST).as_mut() {
|
match self.view.get_props(super::COMPONENT_RECENTS_LIST) {
|
||||||
None => None,
|
None => None,
|
||||||
Some(props) => {
|
Some(props) => {
|
||||||
let msg = self.view.update(
|
let msg = self.view.update(
|
||||||
super::COMPONENT_RECENTS_LIST,
|
super::COMPONENT_RECENTS_LIST,
|
||||||
props
|
BookmarkListPropsBuilder::from(props)
|
||||||
.with_texts(TextParts::new(
|
.with_bookmarks(Some(String::from("Recent connections")), bookmarks)
|
||||||
Some(String::from("Recent connections")),
|
|
||||||
Some(bookmarks),
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
msg
|
msg
|
||||||
@@ -437,10 +459,11 @@ impl AuthActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_ERROR,
|
super::COMPONENT_TEXT_ERROR,
|
||||||
Box::new(MsgBox::new(
|
Box::new(MsgBox::new(
|
||||||
PropsBuilder::default()
|
MsgBoxPropsBuilder::default()
|
||||||
.with_foreground(Color::Red)
|
.with_foreground(Color::Red)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Thick, Color::Red)
|
||||||
.bold()
|
.bold()
|
||||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
|
.with_texts(None, vec![TextSpan::from(text)])
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -462,14 +485,15 @@ impl AuthActivity {
|
|||||||
// Protocol
|
// Protocol
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_QUIT,
|
super::COMPONENT_RADIO_QUIT,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::Yellow)
|
.with_color(Color::Yellow)
|
||||||
.with_background(Color::Black)
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||||
.with_texts(TextParts::new(
|
.with_inverted_color(Color::Black)
|
||||||
Some(String::from("Quit TermSCP?")),
|
.with_options(
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
Some(String::from("Quit termscp?")),
|
||||||
))
|
vec![String::from("Yes"), String::from("No")],
|
||||||
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -489,15 +513,16 @@ impl AuthActivity {
|
|||||||
pub(super) fn mount_bookmark_del_dialog(&mut self) {
|
pub(super) fn mount_bookmark_del_dialog(&mut self) {
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::Yellow)
|
.with_color(Color::Yellow)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Delete bookmark?")),
|
Some(String::from("Delete bookmark?")),
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
vec![String::from("Yes"), String::from("No")],
|
||||||
))
|
)
|
||||||
.with_value(PropValue::Unsigned(1))
|
.with_value(1)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -520,15 +545,16 @@ impl AuthActivity {
|
|||||||
pub(super) fn mount_recent_del_dialog(&mut self) {
|
pub(super) fn mount_recent_del_dialog(&mut self) {
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
|
super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::Yellow)
|
.with_color(Color::Yellow)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Delete bookmark?")),
|
Some(String::from("Delete bookmark?")),
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
vec![String::from("Yes"), String::from("No")],
|
||||||
))
|
)
|
||||||
.with_value(PropValue::Unsigned(1))
|
.with_value(1)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -550,27 +576,31 @@ impl AuthActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_BOOKMARK_NAME,
|
super::COMPONENT_INPUT_BOOKMARK_NAME,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_foreground(Color::LightCyan)
|
.with_foreground(Color::LightCyan)
|
||||||
.with_texts(TextParts::new(
|
.with_label(String::from("Save bookmark as..."))
|
||||||
Some(String::from("Save bookmark as...")),
|
.with_borders(
|
||||||
None,
|
Borders::TOP | Borders::RIGHT | Borders::LEFT,
|
||||||
))
|
BorderType::Rounded,
|
||||||
//.with_borders(Borders::TOP | Borders::RIGHT | Borders::LEFT)
|
Color::Reset,
|
||||||
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
|
super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::Red)
|
.with_color(Color::Red)
|
||||||
//.with_borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT)
|
.with_borders(
|
||||||
.with_texts(TextParts::new(
|
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
|
||||||
|
BorderType::Rounded,
|
||||||
|
Color::Reset,
|
||||||
|
)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Save password?")),
|
Some(String::from("Save password?")),
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
vec![String::from("Yes"), String::from("No")],
|
||||||
))
|
)
|
||||||
//.with_value(PropValue::Unsigned(1))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -593,8 +623,9 @@ impl AuthActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_HELP,
|
super::COMPONENT_TEXT_HELP,
|
||||||
Box::new(Table::new(
|
Box::new(Table::new(
|
||||||
PropsBuilder::default()
|
TablePropsBuilder::default()
|
||||||
.with_texts(TextParts::table(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
|
.with_table(
|
||||||
Some(String::from("Help")),
|
Some(String::from("Help")),
|
||||||
TableBuilder::default()
|
TableBuilder::default()
|
||||||
.add_col(
|
.add_col(
|
||||||
@@ -603,7 +634,7 @@ impl AuthActivity {
|
|||||||
.with_foreground(Color::Cyan)
|
.with_foreground(Color::Cyan)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.add_col(TextSpan::from(" Quit TermSCP"))
|
.add_col(TextSpan::from(" Quit termscp"))
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(
|
.add_col(
|
||||||
TextSpanBuilder::new("<TAB>")
|
TextSpanBuilder::new("<TAB>")
|
||||||
@@ -661,7 +692,7 @@ impl AuthActivity {
|
|||||||
)
|
)
|
||||||
.add_col(TextSpan::from(" Save bookmark"))
|
.add_col(TextSpan::from(" Save bookmark"))
|
||||||
.build(),
|
.build(),
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -680,32 +711,46 @@ impl AuthActivity {
|
|||||||
///
|
///
|
||||||
/// Collect input values from view
|
/// Collect input values from view
|
||||||
pub(super) fn get_input(&self) -> (String, u16, FileTransferProtocol, String, String) {
|
pub(super) fn get_input(&self) -> (String, u16, FileTransferProtocol, String, String) {
|
||||||
let addr: String = match self.view.get_value(super::COMPONENT_INPUT_ADDR) {
|
let addr: String = self.get_input_addr();
|
||||||
Some(Payload::Text(a)) => a,
|
let port: u16 = self.get_input_port();
|
||||||
_ => String::new(),
|
let protocol: FileTransferProtocol = self.get_input_protocol();
|
||||||
};
|
let username: String = self.get_input_username();
|
||||||
let port: u16 = match self.view.get_value(super::COMPONENT_INPUT_PORT) {
|
let password: String = self.get_input_password();
|
||||||
Some(Payload::Unsigned(p)) => p as u16,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
let protocol: FileTransferProtocol =
|
|
||||||
match self.view.get_value(super::COMPONENT_RADIO_PROTOCOL) {
|
|
||||||
Some(Payload::Unsigned(p)) => match p {
|
|
||||||
1 => FileTransferProtocol::Scp,
|
|
||||||
2 => FileTransferProtocol::Ftp(false),
|
|
||||||
3 => FileTransferProtocol::Ftp(true),
|
|
||||||
_ => FileTransferProtocol::Sftp,
|
|
||||||
},
|
|
||||||
_ => FileTransferProtocol::Sftp,
|
|
||||||
};
|
|
||||||
let username: String = match self.view.get_value(super::COMPONENT_INPUT_USERNAME) {
|
|
||||||
Some(Payload::Text(a)) => a,
|
|
||||||
_ => String::new(),
|
|
||||||
};
|
|
||||||
let password: String = match self.view.get_value(super::COMPONENT_INPUT_PASSWORD) {
|
|
||||||
Some(Payload::Text(a)) => a,
|
|
||||||
_ => String::new(),
|
|
||||||
};
|
|
||||||
(addr, port, protocol, username, password)
|
(addr, port, protocol, username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_input_addr(&self) -> String {
|
||||||
|
match self.view.get_state(super::COMPONENT_INPUT_ADDR) {
|
||||||
|
Some(Payload::One(Value::Str(x))) => x,
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_input_port(&self) -> u16 {
|
||||||
|
match self.view.get_state(super::COMPONENT_INPUT_PORT) {
|
||||||
|
Some(Payload::One(Value::Usize(x))) => x as u16,
|
||||||
|
_ => Self::get_default_port_for_protocol(FileTransferProtocol::Sftp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_input_protocol(&self) -> FileTransferProtocol {
|
||||||
|
match self.view.get_state(super::COMPONENT_RADIO_PROTOCOL) {
|
||||||
|
Some(Payload::One(Value::Usize(x))) => Self::protocol_opt_to_enum(x),
|
||||||
|
_ => FileTransferProtocol::Sftp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_input_username(&self) -> String {
|
||||||
|
match self.view.get_state(super::COMPONENT_INPUT_USERNAME) {
|
||||||
|
Some(Payload::One(Value::Str(x))) => x,
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_input_password(&self) -> String {
|
||||||
|
match self.view.get_state(super::COMPONENT_INPUT_PASSWORD) {
|
||||||
|
Some(Payload::One(Value::Str(x))) => x,
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// locals
|
||||||
|
use super::{FileTransferActivity, FsEntry};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
/// ### action_enter_local_dir
|
||||||
|
///
|
||||||
|
/// Enter a directory on local host from entry
|
||||||
|
/// Return true whether the directory changed
|
||||||
|
pub(crate) fn action_enter_local_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
|
||||||
|
match entry {
|
||||||
|
FsEntry::Directory(dir) => {
|
||||||
|
self.local_changedir(dir.abs_path.as_path(), true);
|
||||||
|
if self.browser.sync_browsing && !block_sync {
|
||||||
|
self.action_change_remote_dir(dir.name, true);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
FsEntry::File(file) => {
|
||||||
|
match &file.symlink {
|
||||||
|
Some(symlink_entry) => {
|
||||||
|
// If symlink and is directory, point to symlink
|
||||||
|
match &**symlink_entry {
|
||||||
|
FsEntry::Directory(dir) => {
|
||||||
|
self.local_changedir(dir.abs_path.as_path(), true);
|
||||||
|
// Check whether to sync
|
||||||
|
if self.browser.sync_browsing && !block_sync {
|
||||||
|
self.action_change_remote_dir(dir.name.clone(), true);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### action_enter_remote_dir
|
||||||
|
///
|
||||||
|
/// Enter a directory on local host from entry
|
||||||
|
/// Return true whether the directory changed
|
||||||
|
pub(crate) fn action_enter_remote_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
|
||||||
|
match entry {
|
||||||
|
FsEntry::Directory(dir) => {
|
||||||
|
self.remote_changedir(dir.abs_path.as_path(), true);
|
||||||
|
if self.browser.sync_browsing && !block_sync {
|
||||||
|
self.action_change_local_dir(dir.name, true);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
FsEntry::File(file) => {
|
||||||
|
match &file.symlink {
|
||||||
|
Some(symlink_entry) => {
|
||||||
|
// If symlink and is directory, point to symlink
|
||||||
|
match &**symlink_entry {
|
||||||
|
FsEntry::Directory(dir) => {
|
||||||
|
self.remote_changedir(dir.abs_path.as_path(), true);
|
||||||
|
// Check whether to sync
|
||||||
|
if self.browser.sync_browsing && !block_sync {
|
||||||
|
self.action_change_local_dir(dir.name.clone(), true);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### action_change_local_dir
|
||||||
|
///
|
||||||
|
/// Change local directory reading value from input
|
||||||
|
pub(crate) fn action_change_local_dir(&mut self, input: String, block_sync: bool) {
|
||||||
|
let dir_path: PathBuf = self.local_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||||
|
self.local_changedir(dir_path.as_path(), true);
|
||||||
|
// Check whether to sync
|
||||||
|
if self.browser.sync_browsing && !block_sync {
|
||||||
|
self.action_change_remote_dir(input, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### action_change_remote_dir
|
||||||
|
///
|
||||||
|
/// Change remote directory reading value from input
|
||||||
|
pub(crate) fn action_change_remote_dir(&mut self, input: String, block_sync: bool) {
|
||||||
|
let dir_path: PathBuf = self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||||
|
self.remote_changedir(dir_path.as_path(), true);
|
||||||
|
// Check whether to sync
|
||||||
|
if self.browser.sync_browsing && !block_sync {
|
||||||
|
self.action_change_local_dir(input, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### action_go_to_previous_local_dir
|
||||||
|
///
|
||||||
|
/// Go to previous directory from localhost
|
||||||
|
pub(crate) fn action_go_to_previous_local_dir(&mut self, block_sync: bool) {
|
||||||
|
if let Some(d) = self.local_mut().popd() {
|
||||||
|
self.local_changedir(d.as_path(), false);
|
||||||
|
// Check whether to sync
|
||||||
|
if self.browser.sync_browsing && !block_sync {
|
||||||
|
self.action_go_to_previous_remote_dir(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### action_go_to_previous_remote_dir
|
||||||
|
///
|
||||||
|
/// Go to previous directory from remote host
|
||||||
|
pub(crate) fn action_go_to_previous_remote_dir(&mut self, block_sync: bool) {
|
||||||
|
if let Some(d) = self.remote_mut().popd() {
|
||||||
|
self.remote_changedir(d.as_path(), false);
|
||||||
|
// Check whether to sync
|
||||||
|
if self.browser.sync_browsing && !block_sync {
|
||||||
|
self.action_go_to_previous_local_dir(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### action_go_to_local_upper_dir
|
||||||
|
///
|
||||||
|
/// Go to upper directory on local host
|
||||||
|
pub(crate) fn action_go_to_local_upper_dir(&mut self, block_sync: bool) {
|
||||||
|
// Get pwd
|
||||||
|
let path: PathBuf = self.local().wrkdir.clone();
|
||||||
|
// Go to parent directory
|
||||||
|
if let Some(parent) = path.as_path().parent() {
|
||||||
|
self.local_changedir(parent, true);
|
||||||
|
// If sync is enabled update remote too
|
||||||
|
if self.browser.sync_browsing && !block_sync {
|
||||||
|
self.action_go_to_remote_upper_dir(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #### action_go_to_remote_upper_dir
|
||||||
|
///
|
||||||
|
/// Go to upper directory on remote host
|
||||||
|
pub(crate) fn action_go_to_remote_upper_dir(&mut self, block_sync: bool) {
|
||||||
|
// Get pwd
|
||||||
|
let path: PathBuf = self.remote().wrkdir.clone();
|
||||||
|
// Go to parent directory
|
||||||
|
if let Some(parent) = path.as_path().parent() {
|
||||||
|
self.remote_changedir(parent, true);
|
||||||
|
// If sync is enabled update local too
|
||||||
|
if self.browser.sync_browsing && !block_sync {
|
||||||
|
self.action_go_to_local_upper_dir(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
extern crate tempfile;
|
||||||
|
// locals
|
||||||
|
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
||||||
|
use crate::filetransfer::FileTransferErrorType;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
/// ### action_local_copy
|
||||||
|
///
|
||||||
|
/// Copy file on local
|
||||||
|
pub(crate) fn action_local_copy(&mut self, input: String) {
|
||||||
|
match self.get_local_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
let dest_path: PathBuf = PathBuf::from(input);
|
||||||
|
self.local_copy_file(&entry, dest_path.as_path());
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::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();
|
||||||
|
dest_path.push(entry.get_name());
|
||||||
|
self.local_copy_file(entry, dest_path.as_path());
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### action_remote_copy
|
||||||
|
///
|
||||||
|
/// Copy file on remote
|
||||||
|
pub(crate) fn action_remote_copy(&mut self, input: String) {
|
||||||
|
match self.get_remote_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
let dest_path: PathBuf = PathBuf::from(input);
|
||||||
|
self.remote_copy_file(&entry, dest_path.as_path());
|
||||||
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::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();
|
||||||
|
dest_path.push(entry.get_name());
|
||||||
|
self.remote_copy_file(entry, dest_path.as_path());
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_copy_file(&mut self, entry: &FsEntry, dest: &Path) {
|
||||||
|
match self.host.copy(entry, dest) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"Copied \"{}\" to \"{}\"",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not copy \"{}\" to \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote_copy_file(&mut self, entry: &FsEntry, dest: &Path) {
|
||||||
|
match self.client.as_mut().copy(entry, dest) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"Copied \"{}\" to \"{}\"",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => match err.kind() {
|
||||||
|
FileTransferErrorType::UnsupportedFeature => {
|
||||||
|
// If copy is not supported, perform the tricky copy
|
||||||
|
self.tricky_copy(entry, dest);
|
||||||
|
}
|
||||||
|
_ => self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not copy \"{}\" to \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// locals
|
||||||
|
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_local_delete(&mut self) {
|
||||||
|
match self.get_local_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
// Delete file
|
||||||
|
self.local_remove_file(&entry);
|
||||||
|
// Reload
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::Many(entries) => {
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
// Delete file
|
||||||
|
self.local_remove_file(entry);
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_remote_delete(&mut self) {
|
||||||
|
match self.get_remote_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
// Delete file
|
||||||
|
self.remote_remove_file(&entry);
|
||||||
|
// Reload
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::Many(entries) => {
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
// Delete file
|
||||||
|
self.remote_remove_file(entry);
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn local_remove_file(&mut self, entry: &FsEntry) {
|
||||||
|
match self.host.remove(&entry) {
|
||||||
|
Ok(_) => {
|
||||||
|
// Log
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Removed file \"{}\"", entry.get_abs_path().display()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not delete file \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remote_remove_file(&mut self, entry: &FsEntry) {
|
||||||
|
match self.client.remove(&entry) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Removed file \"{}\"", entry.get_abs_path().display()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not delete file \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// locals
|
||||||
|
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_edit_local_file(&mut self) {
|
||||||
|
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => vec![entry],
|
||||||
|
SelectedEntry::Many(entries) => entries,
|
||||||
|
SelectedEntry::None => vec![],
|
||||||
|
};
|
||||||
|
// Edit all entries
|
||||||
|
for entry in entries.iter() {
|
||||||
|
// Check if file
|
||||||
|
if entry.is_file() {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Opening file \"{}\"...", entry.get_abs_path().display()),
|
||||||
|
);
|
||||||
|
// Edit file
|
||||||
|
if let Err(err) = self.edit_local_file(entry.get_abs_path().as_path()) {
|
||||||
|
self.log_and_alert(LogLevel::Error, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_edit_remote_file(&mut self) {
|
||||||
|
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => vec![entry],
|
||||||
|
SelectedEntry::Many(entries) => entries,
|
||||||
|
SelectedEntry::None => vec![],
|
||||||
|
};
|
||||||
|
// Edit all entries
|
||||||
|
for entry in entries.iter() {
|
||||||
|
// Check if file
|
||||||
|
if let FsEntry::File(file) = entry {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Opening file \"{}\"...", entry.get_abs_path().display()),
|
||||||
|
);
|
||||||
|
// Edit file
|
||||||
|
if let Err(err) = self.edit_remote_file(&file) {
|
||||||
|
self.log_and_alert(LogLevel::Error, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// locals
|
||||||
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_local_exec(&mut self, input: String) {
|
||||||
|
match self.host.exec(input.as_str()) {
|
||||||
|
Ok(output) => {
|
||||||
|
// Reload files
|
||||||
|
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Report err
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not execute command \"{}\": {}", input, err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
||||||
|
match self.client.as_mut().exec(input.as_str()) {
|
||||||
|
Ok(output) => {
|
||||||
|
// Reload files
|
||||||
|
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Report err
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not execute command \"{}\": {}", input, err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// locals
|
||||||
|
use super::super::browser::FileExplorerTab;
|
||||||
|
use super::{FileTransferActivity, FsEntry, SelectedEntry};
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> {
|
||||||
|
match self.host.find(input.as_str()) {
|
||||||
|
Ok(entries) => Ok(entries),
|
||||||
|
Err(err) => Err(format!("Could not search for files: {}", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> {
|
||||||
|
match self.client.as_mut().find(input.as_str()) {
|
||||||
|
Ok(entries) => Ok(entries),
|
||||||
|
Err(err) => Err(format!("Could not search for files: {}", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_find_changedir(&mut self) {
|
||||||
|
// Match entry
|
||||||
|
if let SelectedEntry::One(entry) = self.get_found_selected_entries() {
|
||||||
|
// Get path: if a directory, use directory path; if it is a File, get parent path
|
||||||
|
let path: PathBuf = match entry {
|
||||||
|
FsEntry::Directory(dir) => dir.abs_path,
|
||||||
|
FsEntry::File(file) => match file.abs_path.parent() {
|
||||||
|
None => PathBuf::from("."),
|
||||||
|
Some(p) => p.to_path_buf(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Change directory
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||||
|
self.local_changedir(path.as_path(), true)
|
||||||
|
}
|
||||||
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
|
self.remote_changedir(path.as_path(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_find_transfer(&mut self, save_as: Option<String>) {
|
||||||
|
let wrkdir: PathBuf = match self.browser.tab() {
|
||||||
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(),
|
||||||
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(),
|
||||||
|
};
|
||||||
|
match self.get_found_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => match self.browser.tab() {
|
||||||
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||||
|
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), save_as);
|
||||||
|
}
|
||||||
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
|
self.filetransfer_recv(&entry.get_realfile(), wrkdir.as_path(), save_as);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SelectedEntry::Many(entries) => {
|
||||||
|
// In case of selection: save multiple files in wrkdir/input
|
||||||
|
let mut dest_path: PathBuf = wrkdir;
|
||||||
|
if let Some(save_as) = save_as {
|
||||||
|
dest_path.push(save_as);
|
||||||
|
}
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||||
|
self.filetransfer_send(
|
||||||
|
&entry.get_realfile(),
|
||||||
|
dest_path.as_path(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
|
self.filetransfer_recv(
|
||||||
|
&entry.get_realfile(),
|
||||||
|
dest_path.as_path(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_find_delete(&mut self) {
|
||||||
|
match self.get_found_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
// Delete file
|
||||||
|
self.remove_found_file(&entry);
|
||||||
|
}
|
||||||
|
SelectedEntry::Many(entries) => {
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
// Delete file
|
||||||
|
self.remove_found_file(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_found_file(&mut self, entry: &FsEntry) {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||||
|
self.local_remove_file(entry);
|
||||||
|
}
|
||||||
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
|
self.remote_remove_file(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// locals
|
||||||
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_local_mkdir(&mut self, input: String) {
|
||||||
|
match self.host.mkdir(PathBuf::from(input.as_str()).as_path()) {
|
||||||
|
Ok(_) => {
|
||||||
|
// Reload files
|
||||||
|
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Report err
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not create directory \"{}\": {}", input, err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn action_remote_mkdir(&mut self, input: String) {
|
||||||
|
match self
|
||||||
|
.client
|
||||||
|
.as_mut()
|
||||||
|
.mkdir(PathBuf::from(input.as_str()).as_path())
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
// Reload files
|
||||||
|
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Report err
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not create directory \"{}\": {}", input, err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
pub(self) use super::{FileTransferActivity, FsEntry, LogLevel};
|
||||||
|
use tuirealm::{Payload, Value};
|
||||||
|
|
||||||
|
// actions
|
||||||
|
pub(crate) mod change_dir;
|
||||||
|
pub(crate) mod copy;
|
||||||
|
pub(crate) mod delete;
|
||||||
|
pub(crate) mod edit;
|
||||||
|
pub(crate) mod exec;
|
||||||
|
pub(crate) mod find;
|
||||||
|
pub(crate) mod mkdir;
|
||||||
|
pub(crate) mod newfile;
|
||||||
|
pub(crate) mod rename;
|
||||||
|
pub(crate) mod save;
|
||||||
|
|
||||||
|
pub(crate) enum SelectedEntry {
|
||||||
|
One(FsEntry),
|
||||||
|
Many(Vec<FsEntry>),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SelectedEntryIndex {
|
||||||
|
One(usize),
|
||||||
|
Many(Vec<usize>),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<&FsEntry>> for SelectedEntry {
|
||||||
|
fn from(opt: Option<&FsEntry>) -> Self {
|
||||||
|
match opt {
|
||||||
|
Some(e) => SelectedEntry::One(e.clone()),
|
||||||
|
None => SelectedEntry::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<&FsEntry>> for SelectedEntry {
|
||||||
|
fn from(files: Vec<&FsEntry>) -> Self {
|
||||||
|
SelectedEntry::Many(files.into_iter().cloned().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
/// ### get_local_selected_entries
|
||||||
|
///
|
||||||
|
/// Get local file entry
|
||||||
|
pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry {
|
||||||
|
match self.get_selected_index(super::COMPONENT_EXPLORER_LOCAL) {
|
||||||
|
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)),
|
||||||
|
SelectedEntryIndex::Many(files) => {
|
||||||
|
let files: Vec<&FsEntry> = files
|
||||||
|
.iter()
|
||||||
|
.map(|x| self.local().get(*x)) // Usize to Option<FsEntry>
|
||||||
|
.filter(|x| x.is_some()) // Get only some values
|
||||||
|
.map(|x| x.unwrap()) // Option to FsEntry
|
||||||
|
.collect();
|
||||||
|
SelectedEntry::from(files)
|
||||||
|
}
|
||||||
|
SelectedEntryIndex::None => SelectedEntry::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_remote_selected_entries
|
||||||
|
///
|
||||||
|
/// Get remote file entry
|
||||||
|
pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry {
|
||||||
|
match self.get_selected_index(super::COMPONENT_EXPLORER_REMOTE) {
|
||||||
|
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)),
|
||||||
|
SelectedEntryIndex::Many(files) => {
|
||||||
|
let files: Vec<&FsEntry> = files
|
||||||
|
.iter()
|
||||||
|
.map(|x| self.remote().get(*x)) // Usize to Option<FsEntry>
|
||||||
|
.filter(|x| x.is_some()) // Get only some values
|
||||||
|
.map(|x| x.unwrap()) // Option to FsEntry
|
||||||
|
.collect();
|
||||||
|
SelectedEntry::from(files)
|
||||||
|
}
|
||||||
|
SelectedEntryIndex::None => SelectedEntry::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_remote_selected_entries
|
||||||
|
///
|
||||||
|
/// Get remote file entry
|
||||||
|
pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry {
|
||||||
|
match self.get_selected_index(super::COMPONENT_EXPLORER_FIND) {
|
||||||
|
SelectedEntryIndex::One(idx) => {
|
||||||
|
SelectedEntry::from(self.found().as_ref().unwrap().get(idx))
|
||||||
|
}
|
||||||
|
SelectedEntryIndex::Many(files) => {
|
||||||
|
let files: Vec<&FsEntry> = files
|
||||||
|
.iter()
|
||||||
|
.map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option<FsEntry>
|
||||||
|
.filter(|x| x.is_some()) // Get only some values
|
||||||
|
.map(|x| x.unwrap()) // Option to FsEntry
|
||||||
|
.collect();
|
||||||
|
SelectedEntry::from(files)
|
||||||
|
}
|
||||||
|
SelectedEntryIndex::None => SelectedEntry::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- private
|
||||||
|
|
||||||
|
fn get_selected_index(&self, component: &str) -> SelectedEntryIndex {
|
||||||
|
match self.view.get_state(component) {
|
||||||
|
Some(Payload::One(Value::Usize(idx))) => SelectedEntryIndex::One(idx),
|
||||||
|
Some(Payload::Vec(files)) => {
|
||||||
|
let list: Vec<usize> = files
|
||||||
|
.iter()
|
||||||
|
.map(|x| match x {
|
||||||
|
Value::Usize(v) => *v,
|
||||||
|
_ => 0,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
SelectedEntryIndex::Many(list)
|
||||||
|
}
|
||||||
|
_ => SelectedEntryIndex::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// locals
|
||||||
|
use super::{FileTransferActivity, FsEntry, LogLevel};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_local_newfile(&mut self, input: String) {
|
||||||
|
// Check if file exists
|
||||||
|
let mut file_exists: bool = false;
|
||||||
|
for file in self.local().iter_files_all() {
|
||||||
|
if input == file.get_name() {
|
||||||
|
file_exists = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if file_exists {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Warn,
|
||||||
|
format!("File \"{}\" already exists", input,),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Create file
|
||||||
|
let file_path: PathBuf = PathBuf::from(input.as_str());
|
||||||
|
if let Err(err) = self.host.open_file_write(file_path.as_path()) {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not create file \"{}\": {}", file_path.display(), err),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Created file \"{}\"", file_path.display()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Reload files
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_remote_newfile(&mut self, input: String) {
|
||||||
|
// Check if file exists
|
||||||
|
let mut file_exists: bool = false;
|
||||||
|
for file in self.remote().iter_files_all() {
|
||||||
|
if input == file.get_name() {
|
||||||
|
file_exists = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if file_exists {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Warn,
|
||||||
|
format!("File \"{}\" already exists", input,),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Get path on remote
|
||||||
|
let file_path: PathBuf = PathBuf::from(input.as_str());
|
||||||
|
// Create file (on local)
|
||||||
|
match tempfile::NamedTempFile::new() {
|
||||||
|
Err(err) => self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not create tempfile: {}", err),
|
||||||
|
),
|
||||||
|
Ok(tfile) => {
|
||||||
|
// Stat tempfile
|
||||||
|
let local_file: FsEntry = match self.host.stat(tfile.path()) {
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not stat tempfile: {}", err),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(f) => f,
|
||||||
|
};
|
||||||
|
if let FsEntry::File(local_file) = local_file {
|
||||||
|
// Create file
|
||||||
|
match self.client.send_file(&local_file, file_path.as_path()) {
|
||||||
|
Err(err) => self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not create file \"{}\": {}", file_path.display(), err),
|
||||||
|
),
|
||||||
|
Ok(writer) => {
|
||||||
|
// Finalize write
|
||||||
|
if let Err(err) = self.client.on_sent(writer) {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Warn,
|
||||||
|
format!("Could not finalize file: {}", err),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Created file \"{}\"", file_path.display()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Reload files
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// locals
|
||||||
|
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_local_rename(&mut self, input: String) {
|
||||||
|
match self.get_local_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
let dest_path: PathBuf = PathBuf::from(input);
|
||||||
|
self.local_rename_file(&entry, dest_path.as_path());
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::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();
|
||||||
|
dest_path.push(entry.get_name());
|
||||||
|
self.local_rename_file(entry, dest_path.as_path());
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_remote_rename(&mut self, input: String) {
|
||||||
|
match self.get_remote_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
let dest_path: PathBuf = PathBuf::from(input);
|
||||||
|
self.remote_rename_file(&entry, dest_path.as_path());
|
||||||
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::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();
|
||||||
|
dest_path.push(entry.get_name());
|
||||||
|
self.remote_rename_file(entry, dest_path.as_path());
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_rename_file(&mut self, entry: &FsEntry, dest: &Path) {
|
||||||
|
match self.host.rename(entry, dest) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"Moved \"{}\" to \"{}\"",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not move \"{}\" to \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote_rename_file(&mut self, entry: &FsEntry, dest: &Path) {
|
||||||
|
match self.client.as_mut().rename(entry, dest) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"Moved \"{}\" to \"{}\"",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not move \"{}\" to \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// locals
|
||||||
|
use super::{FileTransferActivity, SelectedEntry};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_local_saveas(&mut self, input: String) {
|
||||||
|
self.action_local_send_file(Some(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_remote_saveas(&mut self, input: String) {
|
||||||
|
self.action_remote_recv_file(Some(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_local_send(&mut self) {
|
||||||
|
self.action_local_send_file(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_remote_recv(&mut self) {
|
||||||
|
self.action_remote_recv_file(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_local_send_file(&mut self, save_as: Option<String>) {
|
||||||
|
let wrkdir: PathBuf = self.remote().wrkdir.clone();
|
||||||
|
match self.get_local_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), save_as);
|
||||||
|
}
|
||||||
|
SelectedEntry::Many(entries) => {
|
||||||
|
// In case of selection: save multiple files in wrkdir/input
|
||||||
|
let mut dest_path: PathBuf = wrkdir;
|
||||||
|
if let Some(save_as) = save_as {
|
||||||
|
dest_path.push(save_as);
|
||||||
|
}
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
self.filetransfer_send(&entry.get_realfile(), dest_path.as_path(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_remote_recv_file(&mut self, save_as: Option<String>) {
|
||||||
|
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
||||||
|
match self.get_remote_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
self.filetransfer_recv(&entry.get_realfile(), wrkdir.as_path(), save_as);
|
||||||
|
}
|
||||||
|
SelectedEntry::Many(entries) => {
|
||||||
|
// In case of selection: save multiple files in wrkdir/input
|
||||||
|
let mut dest_path: PathBuf = wrkdir;
|
||||||
|
if let Some(save_as) = save_as {
|
||||||
|
dest_path.push(save_as);
|
||||||
|
}
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
self.filetransfer_recv(&entry.get_realfile(), dest_path.as_path(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
|
||||||
|
use crate::fs::FsEntry;
|
||||||
|
use crate::system::config_client::ConfigClient;
|
||||||
|
|
||||||
|
/// ## FileExplorerTab
|
||||||
|
///
|
||||||
|
/// File explorer tab
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum FileExplorerTab {
|
||||||
|
Local,
|
||||||
|
Remote,
|
||||||
|
FindLocal, // Find result tab
|
||||||
|
FindRemote, // Find result tab
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Browser
|
||||||
|
///
|
||||||
|
/// Browser contains the browser options
|
||||||
|
pub struct Browser {
|
||||||
|
local: FileExplorer, // Local File explorer state
|
||||||
|
remote: FileExplorer, // Remote File explorer state
|
||||||
|
found: Option<FileExplorer>, // File explorer for find result
|
||||||
|
tab: FileExplorerTab, // Current selected tab
|
||||||
|
pub sync_browsing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Browser {
|
||||||
|
/// ### new
|
||||||
|
///
|
||||||
|
/// Build a new `Browser` struct
|
||||||
|
pub fn new(cli: Option<&ConfigClient>) -> Self {
|
||||||
|
Self {
|
||||||
|
local: Self::build_local_explorer(cli),
|
||||||
|
remote: Self::build_remote_explorer(cli),
|
||||||
|
found: None,
|
||||||
|
tab: FileExplorerTab::Local,
|
||||||
|
sync_browsing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(&self) -> &FileExplorer {
|
||||||
|
&self.local
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local_mut(&mut self) -> &mut FileExplorer {
|
||||||
|
&mut self.local
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remote(&self) -> &FileExplorer {
|
||||||
|
&self.remote
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remote_mut(&mut self) -> &mut FileExplorer {
|
||||||
|
&mut self.remote
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn found(&self) -> Option<&FileExplorer> {
|
||||||
|
self.found.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn found_mut(&mut self) -> Option<&mut FileExplorer> {
|
||||||
|
self.found.as_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_found(&mut self, files: Vec<FsEntry>) {
|
||||||
|
let mut explorer = Self::build_found_explorer();
|
||||||
|
explorer.set_files(files);
|
||||||
|
self.found = Some(explorer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn del_found(&mut self) {
|
||||||
|
self.found = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tab(&self) -> FileExplorerTab {
|
||||||
|
self.tab
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### change_tab
|
||||||
|
///
|
||||||
|
/// Update tab value
|
||||||
|
pub fn change_tab(&mut self, tab: FileExplorerTab) {
|
||||||
|
self.tab = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### toggle_sync_browsing
|
||||||
|
///
|
||||||
|
/// Invert the current state for the sync browsing
|
||||||
|
pub fn toggle_sync_browsing(&mut self) {
|
||||||
|
self.sync_browsing = !self.sync_browsing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### build_local_explorer
|
||||||
|
///
|
||||||
|
/// Build a file explorer with local host setup
|
||||||
|
pub fn build_local_explorer(cli: Option<&ConfigClient>) -> FileExplorer {
|
||||||
|
let mut builder = Self::build_explorer(cli);
|
||||||
|
if let Some(cli) = cli {
|
||||||
|
builder.with_formatter(cli.get_local_file_fmt().as_deref());
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### build_remote_explorer
|
||||||
|
///
|
||||||
|
/// Build a file explorer with remote host setup
|
||||||
|
pub fn build_remote_explorer(cli: Option<&ConfigClient>) -> FileExplorer {
|
||||||
|
let mut builder = Self::build_explorer(cli);
|
||||||
|
if let Some(cli) = cli {
|
||||||
|
builder.with_formatter(cli.get_remote_file_fmt().as_deref());
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### build_explorer
|
||||||
|
///
|
||||||
|
/// Build explorer reading configuration from `ConfigClient`
|
||||||
|
fn build_explorer(cli: Option<&ConfigClient>) -> FileExplorerBuilder {
|
||||||
|
let mut builder: FileExplorerBuilder = FileExplorerBuilder::new();
|
||||||
|
// Set common keys
|
||||||
|
builder
|
||||||
|
.with_file_sorting(FileSorting::ByName)
|
||||||
|
.with_stack_size(16);
|
||||||
|
match &cli {
|
||||||
|
Some(cli) => {
|
||||||
|
builder // Build according to current configuration
|
||||||
|
.with_group_dirs(cli.get_group_dirs())
|
||||||
|
.with_hidden_files(cli.get_show_hidden_files());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
builder // Build default
|
||||||
|
.with_group_dirs(Some(GroupDirs::First));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### build_found_explorer
|
||||||
|
///
|
||||||
|
/// Build explorer reading from `ConfigClient`, for found result (has some differences)
|
||||||
|
fn build_found_explorer() -> FileExplorer {
|
||||||
|
FileExplorerBuilder::new()
|
||||||
|
.with_file_sorting(FileSorting::ByName)
|
||||||
|
.with_group_dirs(Some(GroupDirs::First))
|
||||||
|
.with_hidden_files(true)
|
||||||
|
.with_stack_size(0)
|
||||||
|
.with_formatter(Some("{NAME} {SYMLINK}"))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
pub(crate) mod browser;
|
||||||
|
pub(crate) mod transfer;
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
use bytesize::ByteSize;
|
||||||
|
use std::fmt;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// ### TransferStates
|
||||||
|
///
|
||||||
|
/// TransferStates contains the states related to the transfer process
|
||||||
|
pub struct TransferStates {
|
||||||
|
aborted: bool, // Describes whether the transfer process has been aborted
|
||||||
|
pub full: ProgressStates, // full transfer states
|
||||||
|
pub partial: ProgressStates, // Partial transfer states
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### ProgressStates
|
||||||
|
///
|
||||||
|
/// Progress states describes the states for the progress of a single transfer part
|
||||||
|
pub struct ProgressStates {
|
||||||
|
started: Instant,
|
||||||
|
total: usize,
|
||||||
|
written: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TransferStates {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransferStates {
|
||||||
|
/// ### new
|
||||||
|
///
|
||||||
|
/// Instantiates a new transfer states
|
||||||
|
pub fn new() -> TransferStates {
|
||||||
|
TransferStates {
|
||||||
|
aborted: false,
|
||||||
|
full: ProgressStates::default(),
|
||||||
|
partial: ProgressStates::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### reset
|
||||||
|
///
|
||||||
|
/// Re-intiialize transfer states
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.aborted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### abort
|
||||||
|
///
|
||||||
|
/// Set aborted to true
|
||||||
|
pub fn abort(&mut self) {
|
||||||
|
self.aborted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### aborted
|
||||||
|
///
|
||||||
|
/// Returns whether transfer has been aborted
|
||||||
|
pub fn aborted(&self) -> bool {
|
||||||
|
self.aborted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProgressStates {
|
||||||
|
fn default() -> Self {
|
||||||
|
ProgressStates {
|
||||||
|
started: Instant::now(),
|
||||||
|
written: 0,
|
||||||
|
total: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ProgressStates {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let eta: String = match self.calc_eta() {
|
||||||
|
0 => String::from("--:--"),
|
||||||
|
seconds => format!(
|
||||||
|
"{:0width$}:{:0width$}",
|
||||||
|
(seconds / 60),
|
||||||
|
(seconds % 60),
|
||||||
|
width = 2
|
||||||
|
),
|
||||||
|
};
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{:.2}% - ETA {} ({}/s)",
|
||||||
|
self.calc_progress_percentage(),
|
||||||
|
eta,
|
||||||
|
ByteSize(self.calc_bytes_per_second())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressStates {
|
||||||
|
/// ### init
|
||||||
|
///
|
||||||
|
/// Initialize a new Progress State
|
||||||
|
pub fn init(&mut self, sz: usize) {
|
||||||
|
self.started = Instant::now();
|
||||||
|
self.total = sz;
|
||||||
|
self.written = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### update_progress
|
||||||
|
///
|
||||||
|
/// Update progress state
|
||||||
|
pub fn update_progress(&mut self, delta: usize) -> f64 {
|
||||||
|
self.written += delta;
|
||||||
|
self.calc_progress_percentage()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### calc_progress
|
||||||
|
///
|
||||||
|
/// Calculate progress in a range between 0.0 to 1.0
|
||||||
|
pub fn calc_progress(&self) -> f64 {
|
||||||
|
let prog: f64 = (self.written as f64) / (self.total as f64);
|
||||||
|
match prog > 1.0 {
|
||||||
|
true => 1.0,
|
||||||
|
false => prog,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### started
|
||||||
|
///
|
||||||
|
/// Get started
|
||||||
|
pub fn started(&self) -> Instant {
|
||||||
|
self.started
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### calc_progress_percentage
|
||||||
|
///
|
||||||
|
/// Calculate the current transfer progress as percentage
|
||||||
|
fn calc_progress_percentage(&self) -> f64 {
|
||||||
|
self.calc_progress() * 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### calc_bytes_per_second
|
||||||
|
///
|
||||||
|
/// Generic function to calculate bytes per second using elapsed time since transfer started and the bytes written
|
||||||
|
/// and the total amount of bytes to write
|
||||||
|
pub fn calc_bytes_per_second(&self) -> u64 {
|
||||||
|
// bytes_written : elapsed_secs = x : 1
|
||||||
|
let elapsed_secs: u64 = self.started.elapsed().as_secs();
|
||||||
|
match elapsed_secs {
|
||||||
|
0 => match self.written == self.total {
|
||||||
|
// NOTE: would divide by 0 :D
|
||||||
|
true => self.total as u64, // Download completed in less than 1 second
|
||||||
|
false => 0, // 0 B/S
|
||||||
|
},
|
||||||
|
_ => self.written as u64 / elapsed_secs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### calc_eta
|
||||||
|
///
|
||||||
|
/// Calculate ETA for current transfer as seconds
|
||||||
|
fn calc_eta(&self) -> u64 {
|
||||||
|
let elapsed_secs: u64 = self.started.elapsed().as_secs();
|
||||||
|
let prog: f64 = self.calc_progress_percentage();
|
||||||
|
match prog as u64 {
|
||||||
|
0 => 0,
|
||||||
|
_ => ((elapsed_secs * 100) / (prog as u64)) - elapsed_secs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ui_activities_filetransfer_lib_transfer_progress_states() {
|
||||||
|
let mut states: ProgressStates = ProgressStates::default();
|
||||||
|
assert_eq!(states.total, 0);
|
||||||
|
assert_eq!(states.written, 0);
|
||||||
|
assert!(states.started().elapsed().as_secs() < 5);
|
||||||
|
// Init new transfer
|
||||||
|
states.init(1024);
|
||||||
|
assert_eq!(states.total, 1024);
|
||||||
|
assert_eq!(states.written, 0);
|
||||||
|
assert_eq!(states.calc_bytes_per_second(), 0);
|
||||||
|
assert_eq!(states.calc_eta(), 0);
|
||||||
|
assert_eq!(states.calc_progress_percentage(), 0.0);
|
||||||
|
assert_eq!(states.calc_progress(), 0.0);
|
||||||
|
assert_eq!(states.to_string().as_str(), "0.00% - ETA --:-- (0 B/s)");
|
||||||
|
// Wait 4 second (virtually)
|
||||||
|
states.started = states.started.checked_sub(Duration::from_secs(4)).unwrap();
|
||||||
|
// Update state
|
||||||
|
states.update_progress(256);
|
||||||
|
assert_eq!(states.total, 1024);
|
||||||
|
assert_eq!(states.written, 256);
|
||||||
|
assert_eq!(states.calc_bytes_per_second(), 64); // 256 bytes in 4 seconds
|
||||||
|
assert_eq!(states.calc_eta(), 12); // 16 total sub 4
|
||||||
|
assert_eq!(states.calc_progress_percentage(), 25.0);
|
||||||
|
assert_eq!(states.calc_progress(), 0.25);
|
||||||
|
assert_eq!(states.to_string().as_str(), "25.00% - ETA 00:12 (64 B/s)");
|
||||||
|
// 100%
|
||||||
|
states.started = states.started.checked_sub(Duration::from_secs(12)).unwrap();
|
||||||
|
states.update_progress(768);
|
||||||
|
assert_eq!(states.total, 1024);
|
||||||
|
assert_eq!(states.written, 1024);
|
||||||
|
assert_eq!(states.calc_bytes_per_second(), 64); // 256 bytes in 4 seconds
|
||||||
|
assert_eq!(states.calc_eta(), 0); // 16 total sub 4
|
||||||
|
assert_eq!(states.calc_progress_percentage(), 100.0);
|
||||||
|
assert_eq!(states.calc_progress(), 1.0);
|
||||||
|
assert_eq!(states.to_string().as_str(), "100.00% - ETA --:-- (64 B/s)");
|
||||||
|
// Check if terminated at started
|
||||||
|
states.started = Instant::now();
|
||||||
|
assert_eq!(states.calc_bytes_per_second(), 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ui_activities_filetransfer_lib_transfer_states() {
|
||||||
|
let mut states: TransferStates = TransferStates::default();
|
||||||
|
assert_eq!(states.aborted, false);
|
||||||
|
assert_eq!(states.full.total, 0);
|
||||||
|
assert_eq!(states.full.written, 0);
|
||||||
|
assert!(states.full.started.elapsed().as_secs() < 5);
|
||||||
|
assert_eq!(states.partial.total, 0);
|
||||||
|
assert_eq!(states.partial.written, 0);
|
||||||
|
assert!(states.partial.started.elapsed().as_secs() < 5);
|
||||||
|
// Aborted
|
||||||
|
states.abort();
|
||||||
|
assert_eq!(states.aborted(), true);
|
||||||
|
states.reset();
|
||||||
|
assert_eq!(states.aborted(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,22 +23,29 @@
|
|||||||
*/
|
*/
|
||||||
// Locals
|
// Locals
|
||||||
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
|
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
|
||||||
use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
|
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
use crate::system::sshkey_storage::SshKeyStorage;
|
use crate::system::sshkey_storage::SshKeyStorage;
|
||||||
// Ext
|
// Ext
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
const LOG_CAPACITY: usize = 256;
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// ### log
|
/// ### log
|
||||||
///
|
///
|
||||||
/// Add message to log events
|
/// Add message to log events
|
||||||
pub(super) fn log(&mut self, level: LogLevel, msg: &str) {
|
pub(super) fn log(&mut self, level: LogLevel, msg: String) {
|
||||||
|
// Log to file
|
||||||
|
match level {
|
||||||
|
LogLevel::Error => error!("{}", msg),
|
||||||
|
LogLevel::Info => info!("{}", msg),
|
||||||
|
LogLevel::Warn => warn!("{}", msg),
|
||||||
|
}
|
||||||
// Create log record
|
// Create log record
|
||||||
let record: LogRecord = LogRecord::new(level, msg);
|
let record: LogRecord = LogRecord::new(level, msg);
|
||||||
//Check if history overflows the size
|
//Check if history overflows the size
|
||||||
if self.log_records.len() + 1 > self.log_size {
|
if self.log_records.len() + 1 > LOG_CAPACITY {
|
||||||
self.log_records.pop_back(); // Start cleaning events from back
|
self.log_records.pop_back(); // Start cleaning events from back
|
||||||
}
|
}
|
||||||
// Eventually push front the new record
|
// Eventually push front the new record
|
||||||
@@ -52,8 +59,8 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Add message to log events and also display it as an alert
|
/// Add message to log events and also display it as an alert
|
||||||
pub(super) fn log_and_alert(&mut self, level: LogLevel, msg: String) {
|
pub(super) fn log_and_alert(&mut self, level: LogLevel, msg: String) {
|
||||||
self.log(level, msg.as_str());
|
|
||||||
self.mount_error(msg.as_str());
|
self.mount_error(msg.as_str());
|
||||||
|
self.log(level, msg);
|
||||||
// Update log
|
// Update log
|
||||||
let msg = self.update_logbox();
|
let msg = self.update_logbox();
|
||||||
self.update(msg);
|
self.update(msg);
|
||||||
@@ -91,39 +98,6 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### build_explorer
|
|
||||||
///
|
|
||||||
/// Build explorer reading configuration from `ConfigClient`
|
|
||||||
pub(super) fn build_explorer(cli: Option<&ConfigClient>) -> FileExplorer {
|
|
||||||
match &cli {
|
|
||||||
Some(cli) => FileExplorerBuilder::new() // Build according to current configuration
|
|
||||||
.with_file_sorting(FileSorting::ByName)
|
|
||||||
.with_group_dirs(cli.get_group_dirs())
|
|
||||||
.with_hidden_files(cli.get_show_hidden_files())
|
|
||||||
.with_stack_size(16)
|
|
||||||
.with_formatter(cli.get_file_fmt().as_deref())
|
|
||||||
.build(),
|
|
||||||
None => FileExplorerBuilder::new() // Build default
|
|
||||||
.with_file_sorting(FileSorting::ByName)
|
|
||||||
.with_group_dirs(Some(GroupDirs::First))
|
|
||||||
.with_stack_size(16)
|
|
||||||
.build(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### build_found_explorer
|
|
||||||
///
|
|
||||||
/// Build explorer reading from `ConfigClient`, for found result (has some differences)
|
|
||||||
pub(super) fn build_found_explorer() -> FileExplorer {
|
|
||||||
FileExplorerBuilder::new()
|
|
||||||
.with_file_sorting(FileSorting::ByName)
|
|
||||||
.with_group_dirs(Some(GroupDirs::First))
|
|
||||||
.with_hidden_files(true)
|
|
||||||
.with_stack_size(0)
|
|
||||||
.with_formatter(Some("{NAME} {SYMLINK}"))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### setup_text_editor
|
/// ### setup_text_editor
|
||||||
///
|
///
|
||||||
/// Set text editor to use
|
/// Set text editor to use
|
||||||
@@ -150,4 +124,32 @@ impl FileTransferActivity {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### local_to_abs_path
|
||||||
|
///
|
||||||
|
/// Convert a path to absolute according to local explorer
|
||||||
|
pub(super) fn local_to_abs_path(&self, path: &Path) -> PathBuf {
|
||||||
|
match path.is_relative() {
|
||||||
|
true => {
|
||||||
|
let mut d: PathBuf = self.local().wrkdir.clone();
|
||||||
|
d.push(path);
|
||||||
|
d
|
||||||
|
}
|
||||||
|
false => path.to_path_buf(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### remote_to_abs_path
|
||||||
|
///
|
||||||
|
/// Convert a path to absolute according to remote explorer
|
||||||
|
pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf {
|
||||||
|
match path.is_relative() {
|
||||||
|
true => {
|
||||||
|
let mut wrkdir: PathBuf = self.remote().wrkdir.clone();
|
||||||
|
wrkdir.push(path);
|
||||||
|
wrkdir
|
||||||
|
}
|
||||||
|
false => path.to_path_buf(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,17 +26,18 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// This module is split into files, cause it's just too big
|
// This module is split into files, cause it's just too big
|
||||||
mod actions;
|
pub(self) mod actions;
|
||||||
mod misc;
|
pub(self) mod lib;
|
||||||
mod session;
|
pub(self) mod misc;
|
||||||
mod update;
|
pub(self) mod session;
|
||||||
mod view;
|
pub(self) mod update;
|
||||||
|
pub(self) mod view;
|
||||||
|
|
||||||
// Dependencies
|
// Dependencies
|
||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
extern crate crossterm;
|
extern crate crossterm;
|
||||||
extern crate textwrap;
|
extern crate textwrap;
|
||||||
extern crate tui;
|
extern crate tuirealm;
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use super::{Activity, Context, ExitReason};
|
use super::{Activity, Context, ExitReason};
|
||||||
@@ -46,20 +47,22 @@ use crate::filetransfer::sftp_transfer::SftpFileTransfer;
|
|||||||
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
||||||
use crate::fs::explorer::FileExplorer;
|
use crate::fs::explorer::FileExplorer;
|
||||||
use crate::fs::FsEntry;
|
use crate::fs::FsEntry;
|
||||||
|
use crate::host::Localhost;
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
use crate::ui::layout::view::View;
|
pub(crate) use lib::browser;
|
||||||
|
use lib::browser::Browser;
|
||||||
|
use lib::transfer::TransferStates;
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Instant;
|
use tuirealm::View;
|
||||||
|
|
||||||
// -- Storage keys
|
// -- Storage keys
|
||||||
|
|
||||||
const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH";
|
const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH";
|
||||||
const STORAGE_LOGBOX_WIDTH: &str = "LOGBOX_WIDTH";
|
|
||||||
|
|
||||||
// -- components
|
// -- components
|
||||||
|
|
||||||
@@ -67,7 +70,8 @@ const COMPONENT_EXPLORER_LOCAL: &str = "EXPLORER_LOCAL";
|
|||||||
const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE";
|
const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE";
|
||||||
const COMPONENT_EXPLORER_FIND: &str = "EXPLORER_FIND";
|
const COMPONENT_EXPLORER_FIND: &str = "EXPLORER_FIND";
|
||||||
const COMPONENT_LOG_BOX: &str = "LOG_BOX";
|
const COMPONENT_LOG_BOX: &str = "LOG_BOX";
|
||||||
const COMPONENT_PROGRESS_BAR: &str = "PROGRESS_BAR";
|
const COMPONENT_PROGRESS_BAR_FULL: &str = "PROGRESS_BAR_FULL";
|
||||||
|
const COMPONENT_PROGRESS_BAR_PARTIAL: &str = "PROGRESS_BAR_PARTIAL";
|
||||||
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
|
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
|
||||||
const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL";
|
const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL";
|
||||||
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
||||||
@@ -84,18 +88,9 @@ const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
|
|||||||
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
|
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
|
||||||
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
|
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
|
||||||
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";
|
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";
|
||||||
|
const COMPONENT_SPAN_STATUS_BAR: &str = "STATUS_BAR";
|
||||||
const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO";
|
const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO";
|
||||||
|
|
||||||
/// ## FileExplorerTab
|
|
||||||
///
|
|
||||||
/// File explorer tab
|
|
||||||
enum FileExplorerTab {
|
|
||||||
Local,
|
|
||||||
Remote,
|
|
||||||
FindLocal, // Find result tab
|
|
||||||
FindRemote, // Find result tab
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## LogLevel
|
/// ## LogLevel
|
||||||
///
|
///
|
||||||
/// Log level type
|
/// Log level type
|
||||||
@@ -118,90 +113,15 @@ impl LogRecord {
|
|||||||
/// ### new
|
/// ### new
|
||||||
///
|
///
|
||||||
/// Instantiates a new LogRecord
|
/// Instantiates a new LogRecord
|
||||||
pub fn new(level: LogLevel, msg: &str) -> LogRecord {
|
pub fn new(level: LogLevel, msg: String) -> LogRecord {
|
||||||
LogRecord {
|
LogRecord {
|
||||||
time: Local::now(),
|
time: Local::now(),
|
||||||
level,
|
level,
|
||||||
msg: String::from(msg),
|
msg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### TransferStates
|
|
||||||
///
|
|
||||||
/// TransferStates contains the states related to the transfer process
|
|
||||||
struct TransferStates {
|
|
||||||
pub progress: f64, // Current read/write progress (percentage)
|
|
||||||
pub started: Instant, // Instant the transfer process started
|
|
||||||
pub aborted: bool, // Describes whether the transfer process has been aborted
|
|
||||||
pub bytes_written: usize, // Bytes written during transfer
|
|
||||||
pub bytes_total: usize, // Total bytes to write
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransferStates {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiates a new transfer states
|
|
||||||
pub fn new() -> TransferStates {
|
|
||||||
TransferStates {
|
|
||||||
progress: 0.0,
|
|
||||||
started: Instant::now(),
|
|
||||||
aborted: false,
|
|
||||||
bytes_written: 0,
|
|
||||||
bytes_total: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### reset
|
|
||||||
///
|
|
||||||
/// Re-intiialize transfer states
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
self.progress = 0.0;
|
|
||||||
self.started = Instant::now();
|
|
||||||
self.aborted = false;
|
|
||||||
self.bytes_written = 0;
|
|
||||||
self.bytes_total = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### set_progress
|
|
||||||
///
|
|
||||||
/// Calculate progress percentage based on current progress
|
|
||||||
pub fn set_progress(&mut self, w: usize, sz: usize) {
|
|
||||||
self.bytes_written = w;
|
|
||||||
self.bytes_total = sz;
|
|
||||||
let mut prog: f64 = ((self.bytes_written as f64) * 100.0) / (self.bytes_total as f64);
|
|
||||||
// Check value
|
|
||||||
if prog > 100.0 {
|
|
||||||
prog = 100.0;
|
|
||||||
} else if prog < 0.0 {
|
|
||||||
prog = 0.0;
|
|
||||||
}
|
|
||||||
self.progress = prog;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### byte_per_second
|
|
||||||
///
|
|
||||||
/// Calculate bytes per second
|
|
||||||
pub fn bytes_per_second(&self) -> u64 {
|
|
||||||
// bytes_written : elapsed_secs = x : 1
|
|
||||||
let elapsed_secs: u64 = self.started.elapsed().as_secs();
|
|
||||||
match elapsed_secs {
|
|
||||||
0 => match self.bytes_written == self.bytes_total {
|
|
||||||
// NOTE: would divide by 0 :D
|
|
||||||
true => self.bytes_total as u64, // Download completed in less than 1 second
|
|
||||||
false => 0, // 0 B/S
|
|
||||||
},
|
|
||||||
_ => self.bytes_written as u64 / elapsed_secs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TransferStates {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## FileTransferActivity
|
/// ## FileTransferActivity
|
||||||
///
|
///
|
||||||
/// FileTransferActivity is the data holder for the file transfer activity
|
/// FileTransferActivity is the data holder for the file transfer activity
|
||||||
@@ -209,13 +129,10 @@ pub struct FileTransferActivity {
|
|||||||
exit_reason: Option<ExitReason>, // Exit reason
|
exit_reason: Option<ExitReason>, // Exit reason
|
||||||
context: Option<Context>, // Context holder
|
context: Option<Context>, // Context holder
|
||||||
view: View, // View
|
view: View, // View
|
||||||
|
host: Localhost, // Localhost
|
||||||
client: Box<dyn FileTransfer>, // File transfer client
|
client: Box<dyn FileTransfer>, // File transfer client
|
||||||
local: FileExplorer, // Local File explorer state
|
browser: Browser, // Browser
|
||||||
remote: FileExplorer, // Remote File explorer state
|
|
||||||
found: Option<FileExplorer>, // File explorer for find result
|
|
||||||
tab: FileExplorerTab, // Current selected tab
|
|
||||||
log_records: VecDeque<LogRecord>, // Log records
|
log_records: VecDeque<LogRecord>, // Log records
|
||||||
log_size: usize, // Log records size (max)
|
|
||||||
transfer: TransferStates, // Transfer states
|
transfer: TransferStates, // Transfer states
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,13 +140,14 @@ impl FileTransferActivity {
|
|||||||
/// ### new
|
/// ### new
|
||||||
///
|
///
|
||||||
/// Instantiates a new FileTransferActivity
|
/// Instantiates a new FileTransferActivity
|
||||||
pub fn new(protocol: FileTransferProtocol) -> FileTransferActivity {
|
pub fn new(host: Localhost, protocol: FileTransferProtocol) -> FileTransferActivity {
|
||||||
// Get config client
|
// Get config client
|
||||||
let config_client: Option<ConfigClient> = Self::init_config_client();
|
let config_client: Option<ConfigClient> = Self::init_config_client();
|
||||||
FileTransferActivity {
|
FileTransferActivity {
|
||||||
exit_reason: None,
|
exit_reason: None,
|
||||||
context: None,
|
context: None,
|
||||||
view: View::init(),
|
view: View::init(),
|
||||||
|
host,
|
||||||
client: match protocol {
|
client: match protocol {
|
||||||
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
|
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
|
||||||
Self::make_ssh_storage(config_client.as_ref()),
|
Self::make_ssh_storage(config_client.as_ref()),
|
||||||
@@ -239,15 +157,35 @@ impl FileTransferActivity {
|
|||||||
Self::make_ssh_storage(config_client.as_ref()),
|
Self::make_ssh_storage(config_client.as_ref()),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
local: Self::build_explorer(config_client.as_ref()),
|
browser: Browser::new(config_client.as_ref()),
|
||||||
remote: Self::build_explorer(config_client.as_ref()),
|
|
||||||
found: None,
|
|
||||||
tab: FileExplorerTab::Local,
|
|
||||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||||
log_size: 256, // Must match with capacity
|
|
||||||
transfer: TransferStates::default(),
|
transfer: TransferStates::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn local(&self) -> &FileExplorer {
|
||||||
|
self.browser.local()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn local_mut(&mut self) -> &mut FileExplorer {
|
||||||
|
self.browser.local_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remote(&self) -> &FileExplorer {
|
||||||
|
self.browser.remote()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remote_mut(&mut self) -> &mut FileExplorer {
|
||||||
|
self.browser.remote_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn found(&self) -> Option<&FileExplorer> {
|
||||||
|
self.browser.found()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn found_mut(&mut self) -> Option<&mut FileExplorer> {
|
||||||
|
self.browser.found_mut()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -262,25 +200,33 @@ impl Activity for FileTransferActivity {
|
|||||||
/// `on_create` is the function which must be called to initialize the activity.
|
/// `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
|
/// `on_create` must initialize all the data structures used by the activity
|
||||||
fn on_create(&mut self, context: Context) {
|
fn on_create(&mut self, context: Context) {
|
||||||
|
debug!("Initializing activity...");
|
||||||
// Set context
|
// Set context
|
||||||
self.context = Some(context);
|
self.context = Some(context);
|
||||||
// Clear terminal
|
// Clear terminal
|
||||||
self.context.as_mut().unwrap().clear_screen();
|
self.context.as_mut().unwrap().clear_screen();
|
||||||
// Put raw mode on enabled
|
// Put raw mode on enabled
|
||||||
let _ = enable_raw_mode();
|
if let Err(err) = enable_raw_mode() {
|
||||||
|
error!("Failed to enter raw mode: {}", err);
|
||||||
|
}
|
||||||
// Set working directory
|
// Set working directory
|
||||||
let pwd: PathBuf = self.context.as_ref().unwrap().local.pwd();
|
let pwd: PathBuf = self.host.pwd();
|
||||||
// Get files at current wd
|
// Get files at current wd
|
||||||
self.local_scan(pwd.as_path());
|
self.local_scan(pwd.as_path());
|
||||||
self.local.wrkdir = pwd;
|
self.local_mut().wrkdir = pwd;
|
||||||
|
debug!("Read working directory");
|
||||||
// Configure text editor
|
// Configure text editor
|
||||||
self.setup_text_editor();
|
self.setup_text_editor();
|
||||||
|
debug!("Setup text editor");
|
||||||
// init view
|
// init view
|
||||||
self.init();
|
self.init();
|
||||||
|
debug!("Initialized view");
|
||||||
// Verify error state from context
|
// Verify error state from context
|
||||||
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||||
|
error!("Fatal error on create: {}", err);
|
||||||
self.mount_fatal(&err);
|
self.mount_fatal(&err);
|
||||||
}
|
}
|
||||||
|
info!("Created FileTransferActivity");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### on_draw
|
/// ### on_draw
|
||||||
@@ -297,6 +243,10 @@ impl Activity for FileTransferActivity {
|
|||||||
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
|
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||||
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
|
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
|
||||||
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
|
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
|
||||||
|
info!(
|
||||||
|
"Client is not connected to remote; connecting to {}:{}",
|
||||||
|
params.address, params.port
|
||||||
|
);
|
||||||
let msg: String = format!("Connecting to {}:{}...", params.address, params.port);
|
let msg: String = format!("Connecting to {}:{}...", params.address, params.port);
|
||||||
// Set init state to connecting popup
|
// Set init state to connecting popup
|
||||||
self.mount_wait(msg.as_str());
|
self.mount_wait(msg.as_str());
|
||||||
@@ -330,7 +280,9 @@ impl Activity for FileTransferActivity {
|
|||||||
/// This function must be called once before terminating the activity.
|
/// This function must be called once before terminating the activity.
|
||||||
fn on_destroy(&mut self) -> Option<Context> {
|
fn on_destroy(&mut self) -> Option<Context> {
|
||||||
// Disable raw mode
|
// Disable raw mode
|
||||||
let _ = disable_raw_mode();
|
if let Err(err) = disable_raw_mode() {
|
||||||
|
error!("Failed to disable raw mode: {}", err);
|
||||||
|
}
|
||||||
// Disconnect client
|
// Disconnect client
|
||||||
if self.client.is_connected() {
|
if self.client.is_connected() {
|
||||||
let _ = self.client.disconnect();
|
let _ = self.client.disconnect();
|
||||||
@@ -29,25 +29,27 @@
|
|||||||
extern crate bytesize;
|
extern crate bytesize;
|
||||||
// locals
|
// locals
|
||||||
use super::{
|
use super::{
|
||||||
FileExplorerTab, FileTransferActivity, LogLevel, COMPONENT_EXPLORER_FIND,
|
actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel,
|
||||||
COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY,
|
COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE,
|
||||||
COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR,
|
COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO,
|
||||||
COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS,
|
COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS,
|
||||||
COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR, COMPONENT_RADIO_DELETE,
|
COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL,
|
||||||
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING,
|
COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE, COMPONENT_RADIO_DISCONNECT,
|
||||||
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
|
COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL,
|
||||||
|
COMPONENT_TEXT_HELP,
|
||||||
};
|
};
|
||||||
use crate::fs::explorer::FileSorting;
|
use crate::fs::explorer::FileSorting;
|
||||||
use crate::fs::FsEntry;
|
use crate::fs::FsEntry;
|
||||||
use crate::ui::activities::keymap::*;
|
use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder};
|
||||||
use crate::ui::layout::props::{
|
use crate::ui::keymap::*;
|
||||||
PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
|
|
||||||
};
|
|
||||||
use crate::ui::layout::{Msg, Payload};
|
|
||||||
// externals
|
// externals
|
||||||
use bytesize::ByteSize;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tui::style::Color;
|
use tuirealm::{
|
||||||
|
components::progress_bar::ProgressBarPropsBuilder,
|
||||||
|
props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
|
||||||
|
tui::style::Color,
|
||||||
|
Msg, Payload, Value,
|
||||||
|
};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
// -- update
|
// -- update
|
||||||
@@ -66,229 +68,144 @@ impl FileTransferActivity {
|
|||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_RIGHT) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_RIGHT) => {
|
||||||
// Change tab
|
// Change tab
|
||||||
self.view.active(COMPONENT_EXPLORER_REMOTE);
|
self.view.active(COMPONENT_EXPLORER_REMOTE);
|
||||||
self.tab = FileExplorerTab::Remote;
|
self.browser.change_tab(FileExplorerTab::Remote);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_BACKSPACE) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_BACKSPACE) => {
|
||||||
// Go to previous directory
|
// Go to previous directory
|
||||||
if let Some(d) = self.local.popd() {
|
self.action_go_to_previous_local_dir(false);
|
||||||
self.local_changedir(d.as_path(), false);
|
if self.browser.sync_browsing {
|
||||||
|
let _ = self.update_remote_filelist();
|
||||||
}
|
}
|
||||||
// Reload file list component
|
// Reload file list component
|
||||||
self.update_local_filelist()
|
self.update_local_filelist()
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, Msg::OnSubmit(Payload::Unsigned(idx))) => {
|
(COMPONENT_EXPLORER_LOCAL, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
|
||||||
// Match selected file
|
// Match selected file
|
||||||
let mut entry: Option<FsEntry> = None;
|
let mut entry: Option<FsEntry> = None;
|
||||||
if let Some(e) = self.local.get(*idx) {
|
if let Some(e) = self.local().get(*idx) {
|
||||||
entry = Some(e.clone());
|
entry = Some(e.clone());
|
||||||
}
|
}
|
||||||
if let Some(entry) = entry {
|
if let Some(entry) = entry {
|
||||||
// If directory, enter directory, otherwise check if symlink
|
if self.action_enter_local_dir(entry, false) {
|
||||||
match entry {
|
// Update file list if sync
|
||||||
FsEntry::Directory(dir) => {
|
if self.browser.sync_browsing {
|
||||||
self.local_changedir(dir.abs_path.as_path(), true);
|
let _ = self.update_remote_filelist();
|
||||||
self.update_local_filelist()
|
|
||||||
}
|
|
||||||
FsEntry::File(file) => {
|
|
||||||
// Check if symlink
|
|
||||||
match &file.symlink {
|
|
||||||
Some(pointer) => match &**pointer {
|
|
||||||
FsEntry::Directory(dir) => {
|
|
||||||
self.local_changedir(dir.abs_path.as_path(), true);
|
|
||||||
self.update_local_filelist()
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
self.update_local_filelist()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_SPACE) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_SPACE) => {
|
||||||
// Get pwd
|
self.action_local_send();
|
||||||
let wrkdir: PathBuf = self.remote.wrkdir.clone();
|
self.update_remote_filelist()
|
||||||
// Get file and clone (due to mutable / immutable stuff...)
|
|
||||||
if self.get_local_file_entry().is_some() {
|
|
||||||
let file: FsEntry = self.get_local_file_entry().unwrap().clone();
|
|
||||||
let name: String = file.get_name().to_string();
|
|
||||||
// Call upload; pass realfile, keep link name
|
|
||||||
self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(name));
|
|
||||||
self.update_remote_filelist()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_A) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_A) => {
|
||||||
// Toggle hidden files
|
// Toggle hidden files
|
||||||
self.local.toggle_hidden_files();
|
self.local_mut().toggle_hidden_files();
|
||||||
// Reload file list component
|
// Reload file list component
|
||||||
self.update_local_filelist()
|
self.update_local_filelist()
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_I) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_I) => {
|
||||||
let file: Option<FsEntry> = self.get_local_file_entry().cloned();
|
if let SelectedEntry::One(file) = self.get_local_selected_entries() {
|
||||||
if let Some(file) = file {
|
|
||||||
self.mount_file_info(&file);
|
self.mount_file_info(&file);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_L) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_L) => {
|
||||||
// Reload directory
|
// Reload directory
|
||||||
let pwd: PathBuf = self.local.wrkdir.clone();
|
let pwd: PathBuf = self.local().wrkdir.clone();
|
||||||
self.local_scan(pwd.as_path());
|
self.local_scan(pwd.as_path());
|
||||||
// Reload file list component
|
// Reload file list component
|
||||||
self.update_local_filelist()
|
self.update_local_filelist()
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_O) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_O) => {
|
||||||
// Clone entry due to mutable stuff...
|
self.action_edit_local_file();
|
||||||
if self.get_local_file_entry().is_some() {
|
|
||||||
let fsentry: FsEntry = self.get_local_file_entry().unwrap().clone();
|
|
||||||
// Check if file
|
|
||||||
if fsentry.is_file() {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Opening file \"{}\"...", fsentry.get_abs_path().display())
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
// Edit file
|
|
||||||
match self.edit_local_file(fsentry.get_abs_path().as_path()) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Reload directory
|
|
||||||
let pwd: PathBuf = self.local.wrkdir.clone();
|
|
||||||
self.local_scan(pwd.as_path());
|
|
||||||
}
|
|
||||||
Err(err) => self.log_and_alert(LogLevel::Error, err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Reload file list component
|
// Reload file list component
|
||||||
self.update_local_filelist()
|
self.update_local_filelist()
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_U) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_U) => {
|
||||||
// Get pwd
|
self.action_go_to_local_upper_dir(false);
|
||||||
let path: PathBuf = self.local.wrkdir.clone();
|
if self.browser.sync_browsing {
|
||||||
// Go to parent directory
|
let _ = self.update_remote_filelist();
|
||||||
if let Some(parent) = path.as_path().parent() {
|
|
||||||
self.local_changedir(parent, true);
|
|
||||||
// Reload file list component
|
|
||||||
}
|
}
|
||||||
|
// Reload file list component
|
||||||
self.update_local_filelist()
|
self.update_local_filelist()
|
||||||
}
|
}
|
||||||
// -- remote tab
|
// -- remote tab
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_LEFT) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_LEFT) => {
|
||||||
// Change tab
|
// Change tab
|
||||||
self.view.active(COMPONENT_EXPLORER_LOCAL);
|
self.view.active(COMPONENT_EXPLORER_LOCAL);
|
||||||
self.tab = FileExplorerTab::Local;
|
self.browser.change_tab(FileExplorerTab::Local);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, Msg::OnSubmit(Payload::Unsigned(idx))) => {
|
(COMPONENT_EXPLORER_REMOTE, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
|
||||||
// Match selected file
|
// Match selected file
|
||||||
let mut entry: Option<FsEntry> = None;
|
let mut entry: Option<FsEntry> = None;
|
||||||
if let Some(e) = self.remote.get(*idx) {
|
if let Some(e) = self.remote().get(*idx) {
|
||||||
entry = Some(e.clone());
|
entry = Some(e.clone());
|
||||||
}
|
}
|
||||||
if let Some(entry) = entry {
|
if let Some(entry) = entry {
|
||||||
// If directory, enter directory; if file, check if is symlink
|
if self.action_enter_remote_dir(entry, false) {
|
||||||
match entry {
|
// Update file list if sync
|
||||||
FsEntry::Directory(dir) => {
|
if self.browser.sync_browsing {
|
||||||
self.remote_changedir(dir.abs_path.as_path(), true);
|
let _ = self.update_local_filelist();
|
||||||
self.update_remote_filelist()
|
|
||||||
}
|
|
||||||
FsEntry::File(file) => {
|
|
||||||
match &file.symlink {
|
|
||||||
Some(symlink_entry) => {
|
|
||||||
// If symlink and is directory, point to symlink
|
|
||||||
match &**symlink_entry {
|
|
||||||
FsEntry::Directory(dir) => {
|
|
||||||
self.remote_changedir(dir.abs_path.as_path(), true);
|
|
||||||
self.update_remote_filelist()
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
self.update_remote_filelist()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_SPACE) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_SPACE) => {
|
||||||
// Get file and clone (due to mutable / immutable stuff...)
|
self.action_remote_recv();
|
||||||
if self.get_remote_file_entry().is_some() {
|
self.update_local_filelist()
|
||||||
let file: FsEntry = self.get_remote_file_entry().unwrap().clone();
|
|
||||||
let name: String = file.get_name().to_string();
|
|
||||||
// Call upload; pass realfile, keep link name
|
|
||||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
|
||||||
self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(name));
|
|
||||||
self.update_local_filelist()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_BACKSPACE) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_BACKSPACE) => {
|
||||||
// Go to previous directory
|
// Go to previous directory
|
||||||
if let Some(d) = self.remote.popd() {
|
self.action_go_to_previous_remote_dir(false);
|
||||||
self.remote_changedir(d.as_path(), false);
|
// If sync is enabled update local too
|
||||||
|
if self.browser.sync_browsing {
|
||||||
|
let _ = self.update_local_filelist();
|
||||||
}
|
}
|
||||||
// Reload file list component
|
// Reload file list component
|
||||||
self.update_remote_filelist()
|
self.update_remote_filelist()
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_A) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_A) => {
|
||||||
// Toggle hidden files
|
// Toggle hidden files
|
||||||
self.remote.toggle_hidden_files();
|
self.remote_mut().toggle_hidden_files();
|
||||||
// Reload file list component
|
// Reload file list component
|
||||||
self.update_remote_filelist()
|
self.update_remote_filelist()
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_I) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_I) => {
|
||||||
let file: Option<FsEntry> = self.get_remote_file_entry().cloned();
|
if let SelectedEntry::One(file) = self.get_remote_selected_entries() {
|
||||||
if let Some(file) = file {
|
|
||||||
self.mount_file_info(&file);
|
self.mount_file_info(&file);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_L) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_L) => {
|
||||||
// Reload directory
|
// Reload directory
|
||||||
let pwd: PathBuf = self.remote.wrkdir.clone();
|
let pwd: PathBuf = self.remote().wrkdir.clone();
|
||||||
self.remote_scan(pwd.as_path());
|
self.remote_scan(pwd.as_path());
|
||||||
// Reload file list component
|
// Reload file list component
|
||||||
self.update_remote_filelist()
|
self.update_remote_filelist()
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_O) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_O) => {
|
||||||
// Clone entry due to mutable stuff...
|
// Edit file
|
||||||
if self.get_remote_file_entry().is_some() {
|
self.action_edit_remote_file();
|
||||||
let fsentry: FsEntry = self.get_remote_file_entry().unwrap().clone();
|
|
||||||
// Check if file
|
|
||||||
if let FsEntry::File(file) = fsentry.clone() {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Opening file \"{}\"...", fsentry.get_abs_path().display())
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
// Edit file
|
|
||||||
match self.edit_remote_file(&file) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Reload directory
|
|
||||||
let pwd: PathBuf = self.remote.wrkdir.clone();
|
|
||||||
self.remote_scan(pwd.as_path());
|
|
||||||
}
|
|
||||||
Err(err) => self.log_and_alert(LogLevel::Error, err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Reload file list component
|
// Reload file list component
|
||||||
self.update_remote_filelist()
|
self.update_remote_filelist()
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_U) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_U) => {
|
||||||
// Get pwd
|
self.action_go_to_remote_upper_dir(false);
|
||||||
let path: PathBuf = self.remote.wrkdir.clone();
|
if self.browser.sync_browsing {
|
||||||
// Go to parent directory
|
let _ = self.update_local_filelist();
|
||||||
if let Some(parent) = path.as_path().parent() {
|
|
||||||
self.remote_changedir(parent, true);
|
|
||||||
}
|
}
|
||||||
// Reload file list component
|
// Reload file list component
|
||||||
self.update_remote_filelist()
|
self.update_remote_filelist()
|
||||||
@@ -355,6 +272,14 @@ impl FileTransferActivity {
|
|||||||
self.mount_exec();
|
self.mount_exec();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_Y)
|
||||||
|
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_Y) => {
|
||||||
|
// Toggle browser sync
|
||||||
|
self.browser.toggle_sync_browsing();
|
||||||
|
// Update status bar
|
||||||
|
self.refresh_status_bar();
|
||||||
|
None
|
||||||
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_ESC)
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_ESC)
|
||||||
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_ESC)
|
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_ESC)
|
||||||
| (COMPONENT_LOG_BOX, &MSG_KEY_ESC) => {
|
| (COMPONENT_LOG_BOX, &MSG_KEY_ESC) => {
|
||||||
@@ -378,15 +303,15 @@ impl FileTransferActivity {
|
|||||||
self.finalize_find();
|
self.finalize_find();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_FIND, Msg::OnSubmit(Payload::Unsigned(idx))) => {
|
(COMPONENT_EXPLORER_FIND, Msg::OnSubmit(_)) => {
|
||||||
// Find changedir
|
// Find changedir
|
||||||
self.action_find_changedir(*idx);
|
self.action_find_changedir();
|
||||||
// Umount find
|
// Umount find
|
||||||
self.umount_find();
|
self.umount_find();
|
||||||
// Finalize find
|
// Finalize find
|
||||||
self.finalize_find();
|
self.finalize_find();
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.update_local_filelist(),
|
FileExplorerTab::Local => self.update_local_filelist(),
|
||||||
FileExplorerTab::Remote => self.update_remote_filelist(),
|
FileExplorerTab::Remote => self.update_remote_filelist(),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -394,17 +319,12 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_FIND, &MSG_KEY_SPACE) => {
|
(COMPONENT_EXPLORER_FIND, &MSG_KEY_SPACE) => {
|
||||||
// Get entry
|
// Get entry
|
||||||
match self.view.get_value(COMPONENT_EXPLORER_FIND) {
|
self.action_find_transfer(None);
|
||||||
Some(Payload::Unsigned(idx)) => {
|
// Reload files
|
||||||
self.action_find_transfer(idx, None);
|
match self.browser.tab() {
|
||||||
// Reload files
|
// NOTE: swapped by purpose
|
||||||
match self.tab {
|
FileExplorerTab::FindLocal => self.update_remote_filelist(),
|
||||||
// NOTE: swapped by purpose
|
FileExplorerTab::FindRemote => self.update_local_filelist(),
|
||||||
FileExplorerTab::FindLocal => self.update_remote_filelist(),
|
|
||||||
FileExplorerTab::FindRemote => self.update_local_filelist(),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,16 +344,16 @@ impl FileTransferActivity {
|
|||||||
self.umount_copy();
|
self.umount_copy();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_COPY, Msg::OnSubmit(Payload::Text(input))) => {
|
(COMPONENT_INPUT_COPY, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||||
// Copy file
|
// Copy file
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_local_copy(input.to_string()),
|
FileExplorerTab::Local => self.action_local_copy(input.to_string()),
|
||||||
FileExplorerTab::Remote => self.action_remote_copy(input.to_string()),
|
FileExplorerTab::Remote => self.action_remote_copy(input.to_string()),
|
||||||
_ => panic!("Found tab doesn't support COPY"),
|
_ => panic!("Found tab doesn't support COPY"),
|
||||||
}
|
}
|
||||||
self.umount_copy();
|
self.umount_copy();
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.update_local_filelist(),
|
FileExplorerTab::Local => self.update_local_filelist(),
|
||||||
FileExplorerTab::Remote => self.update_remote_filelist(),
|
FileExplorerTab::Remote => self.update_remote_filelist(),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -444,16 +364,16 @@ impl FileTransferActivity {
|
|||||||
self.umount_exec();
|
self.umount_exec();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_EXEC, Msg::OnSubmit(Payload::Text(input))) => {
|
(COMPONENT_INPUT_EXEC, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||||
// Exex command
|
// Exex command
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_local_exec(input.to_string()),
|
FileExplorerTab::Local => self.action_local_exec(input.to_string()),
|
||||||
FileExplorerTab::Remote => self.action_remote_exec(input.to_string()),
|
FileExplorerTab::Remote => self.action_remote_exec(input.to_string()),
|
||||||
_ => panic!("Found tab doesn't support EXEC"),
|
_ => panic!("Found tab doesn't support EXEC"),
|
||||||
}
|
}
|
||||||
self.umount_exec();
|
self.umount_exec();
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.update_local_filelist(),
|
FileExplorerTab::Local => self.update_local_filelist(),
|
||||||
FileExplorerTab::Remote => self.update_remote_filelist(),
|
FileExplorerTab::Remote => self.update_remote_filelist(),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -464,10 +384,10 @@ impl FileTransferActivity {
|
|||||||
self.umount_find_input();
|
self.umount_find_input();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_FIND, Msg::OnSubmit(Payload::Text(input))) => {
|
(COMPONENT_INPUT_FIND, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||||
self.umount_find_input();
|
self.umount_find_input();
|
||||||
// Find
|
// Find
|
||||||
let res: Result<Vec<FsEntry>, String> = match self.tab {
|
let res: Result<Vec<FsEntry>, String> = match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_local_find(input.to_string()),
|
FileExplorerTab::Local => self.action_local_find(input.to_string()),
|
||||||
FileExplorerTab::Remote => self.action_remote_find(input.to_string()),
|
FileExplorerTab::Remote => self.action_remote_find(input.to_string()),
|
||||||
_ => panic!("Trying to search for files, while already in a find result"),
|
_ => panic!("Trying to search for files, while already in a find result"),
|
||||||
@@ -480,18 +400,16 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
Ok(files) => {
|
Ok(files) => {
|
||||||
// Create explorer and load files
|
// Create explorer and load files
|
||||||
let mut explorer = Self::build_found_explorer();
|
self.browser.set_found(files);
|
||||||
explorer.set_files(files);
|
|
||||||
self.found = Some(explorer);
|
|
||||||
// Mount result widget
|
// Mount result widget
|
||||||
self.mount_find(input);
|
self.mount_find(input);
|
||||||
self.update_find_list();
|
self.update_find_list();
|
||||||
// Initialize tab
|
// Initialize tab
|
||||||
self.tab = match self.tab {
|
self.browser.change_tab(match self.browser.tab() {
|
||||||
FileExplorerTab::Local => FileExplorerTab::FindLocal,
|
FileExplorerTab::Local => FileExplorerTab::FindLocal,
|
||||||
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
|
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
|
||||||
_ => FileExplorerTab::FindLocal,
|
_ => FileExplorerTab::FindLocal,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -501,16 +419,28 @@ impl FileTransferActivity {
|
|||||||
self.umount_goto();
|
self.umount_goto();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_GOTO, Msg::OnSubmit(Payload::Text(input))) => {
|
(COMPONENT_INPUT_GOTO, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_change_local_dir(input.to_string()),
|
FileExplorerTab::Local => {
|
||||||
FileExplorerTab::Remote => self.action_change_remote_dir(input.to_string()),
|
self.action_change_local_dir(input.to_string(), false)
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote => {
|
||||||
|
self.action_change_remote_dir(input.to_string(), false)
|
||||||
|
}
|
||||||
_ => panic!("Found tab doesn't support GOTO"),
|
_ => panic!("Found tab doesn't support GOTO"),
|
||||||
}
|
}
|
||||||
// Umount
|
// Umount
|
||||||
self.umount_goto();
|
self.umount_goto();
|
||||||
|
// Reload files if sync
|
||||||
|
if self.browser.sync_browsing {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::Remote => self.update_local_filelist(),
|
||||||
|
FileExplorerTab::Local => self.update_remote_filelist(),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
}
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.update_local_filelist(),
|
FileExplorerTab::Local => self.update_local_filelist(),
|
||||||
FileExplorerTab::Remote => self.update_remote_filelist(),
|
FileExplorerTab::Remote => self.update_remote_filelist(),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -521,15 +451,15 @@ impl FileTransferActivity {
|
|||||||
self.umount_mkdir();
|
self.umount_mkdir();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_MKDIR, Msg::OnSubmit(Payload::Text(input))) => {
|
(COMPONENT_INPUT_MKDIR, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_local_mkdir(input.to_string()),
|
FileExplorerTab::Local => self.action_local_mkdir(input.to_string()),
|
||||||
FileExplorerTab::Remote => self.action_remote_mkdir(input.to_string()),
|
FileExplorerTab::Remote => self.action_remote_mkdir(input.to_string()),
|
||||||
_ => panic!("Found tab doesn't support MKDIR"),
|
_ => panic!("Found tab doesn't support MKDIR"),
|
||||||
}
|
}
|
||||||
self.umount_mkdir();
|
self.umount_mkdir();
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.update_local_filelist(),
|
FileExplorerTab::Local => self.update_local_filelist(),
|
||||||
FileExplorerTab::Remote => self.update_remote_filelist(),
|
FileExplorerTab::Remote => self.update_remote_filelist(),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -540,15 +470,15 @@ impl FileTransferActivity {
|
|||||||
self.umount_newfile();
|
self.umount_newfile();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_NEWFILE, Msg::OnSubmit(Payload::Text(input))) => {
|
(COMPONENT_INPUT_NEWFILE, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_local_newfile(input.to_string()),
|
FileExplorerTab::Local => self.action_local_newfile(input.to_string()),
|
||||||
FileExplorerTab::Remote => self.action_remote_newfile(input.to_string()),
|
FileExplorerTab::Remote => self.action_remote_newfile(input.to_string()),
|
||||||
_ => panic!("Found tab doesn't support NEWFILE"),
|
_ => panic!("Found tab doesn't support NEWFILE"),
|
||||||
}
|
}
|
||||||
self.umount_newfile();
|
self.umount_newfile();
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.update_local_filelist(),
|
FileExplorerTab::Local => self.update_local_filelist(),
|
||||||
FileExplorerTab::Remote => self.update_remote_filelist(),
|
FileExplorerTab::Remote => self.update_remote_filelist(),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -559,15 +489,15 @@ impl FileTransferActivity {
|
|||||||
self.umount_rename();
|
self.umount_rename();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_RENAME, Msg::OnSubmit(Payload::Text(input))) => {
|
(COMPONENT_INPUT_RENAME, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_local_rename(input.to_string()),
|
FileExplorerTab::Local => self.action_local_rename(input.to_string()),
|
||||||
FileExplorerTab::Remote => self.action_remote_rename(input.to_string()),
|
FileExplorerTab::Remote => self.action_remote_rename(input.to_string()),
|
||||||
_ => panic!("Found tab doesn't support RENAME"),
|
_ => panic!("Found tab doesn't support RENAME"),
|
||||||
}
|
}
|
||||||
self.umount_rename();
|
self.umount_rename();
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.update_local_filelist(),
|
FileExplorerTab::Local => self.update_local_filelist(),
|
||||||
FileExplorerTab::Remote => self.update_remote_filelist(),
|
FileExplorerTab::Remote => self.update_remote_filelist(),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -578,22 +508,18 @@ impl FileTransferActivity {
|
|||||||
self.umount_saveas();
|
self.umount_saveas();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_SAVEAS, Msg::OnSubmit(Payload::Text(input))) => {
|
(COMPONENT_INPUT_SAVEAS, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_local_saveas(input.to_string()),
|
FileExplorerTab::Local => self.action_local_saveas(input.to_string()),
|
||||||
FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()),
|
FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()),
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||||
// Get entry
|
// Get entry
|
||||||
if let Some(Payload::Unsigned(idx)) =
|
self.action_find_transfer(Some(input.to_string()));
|
||||||
self.view.get_value(COMPONENT_EXPLORER_FIND)
|
|
||||||
{
|
|
||||||
self.action_find_transfer(idx, Some(input.to_string()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.umount_saveas();
|
self.umount_saveas();
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
// NOTE: Swapped is intentional
|
// NOTE: Swapped is intentional
|
||||||
FileExplorerTab::Local => self.update_remote_filelist(),
|
FileExplorerTab::Local => self.update_remote_filelist(),
|
||||||
FileExplorerTab::Remote => self.update_local_filelist(),
|
FileExplorerTab::Remote => self.update_local_filelist(),
|
||||||
@@ -609,30 +535,41 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
// -- delete
|
// -- delete
|
||||||
(COMPONENT_RADIO_DELETE, &MSG_KEY_ESC)
|
(COMPONENT_RADIO_DELETE, &MSG_KEY_ESC)
|
||||||
| (COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::Unsigned(1))) => {
|
| (COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
|
||||||
self.umount_radio_delete();
|
self.umount_radio_delete();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::Unsigned(0))) => {
|
(COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||||
// Choice is 'YES'
|
// Choice is 'YES'
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_local_delete(),
|
FileExplorerTab::Local => self.action_local_delete(),
|
||||||
FileExplorerTab::Remote => self.action_remote_delete(),
|
FileExplorerTab::Remote => self.action_remote_delete(),
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||||
// Get entry
|
// Get entry
|
||||||
if let Some(Payload::Unsigned(idx)) =
|
self.action_find_delete();
|
||||||
self.view.get_value(COMPONENT_EXPLORER_FIND)
|
// Delete entries
|
||||||
{
|
match self.view.get_state(COMPONENT_EXPLORER_FIND) {
|
||||||
self.action_find_delete(idx);
|
Some(Payload::One(Value::Usize(idx))) => {
|
||||||
// Reload entries
|
// Reload entries
|
||||||
self.found.as_mut().unwrap().del_entry(idx);
|
self.found_mut().unwrap().del_entry(idx);
|
||||||
self.update_find_list();
|
}
|
||||||
|
Some(Payload::Vec(values)) => {
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|x| match x {
|
||||||
|
Value::Usize(v) => *v,
|
||||||
|
_ => 0,
|
||||||
|
})
|
||||||
|
.for_each(|x| self.found_mut().unwrap().del_entry(x));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
self.update_find_list();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.umount_radio_delete();
|
self.umount_radio_delete();
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.update_local_filelist(),
|
FileExplorerTab::Local => self.update_local_filelist(),
|
||||||
FileExplorerTab::Remote => self.update_remote_filelist(),
|
FileExplorerTab::Remote => self.update_remote_filelist(),
|
||||||
FileExplorerTab::FindLocal => self.update_local_filelist(),
|
FileExplorerTab::FindLocal => self.update_local_filelist(),
|
||||||
@@ -641,32 +578,33 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
// -- disconnect
|
// -- disconnect
|
||||||
(COMPONENT_RADIO_DISCONNECT, &MSG_KEY_ESC)
|
(COMPONENT_RADIO_DISCONNECT, &MSG_KEY_ESC)
|
||||||
| (COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::Unsigned(1))) => {
|
| (COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
|
||||||
self.umount_disconnect();
|
self.umount_disconnect();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::Unsigned(0))) => {
|
(COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||||
self.disconnect();
|
self.disconnect();
|
||||||
self.umount_disconnect();
|
self.umount_disconnect();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
// -- quit
|
// -- quit
|
||||||
(COMPONENT_RADIO_QUIT, &MSG_KEY_ESC)
|
(COMPONENT_RADIO_QUIT, &MSG_KEY_ESC)
|
||||||
| (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(1))) => {
|
| (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
|
||||||
self.umount_quit();
|
self.umount_quit();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(0))) => {
|
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||||
self.disconnect_and_quit();
|
self.disconnect_and_quit();
|
||||||
self.umount_quit();
|
self.umount_quit();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
// -- sorting
|
// -- sorting
|
||||||
(COMPONENT_RADIO_SORTING, &MSG_KEY_ESC) => {
|
(COMPONENT_RADIO_SORTING, &MSG_KEY_ESC)
|
||||||
|
| (COMPONENT_RADIO_SORTING, Msg::OnSubmit(_)) => {
|
||||||
self.umount_file_sorting();
|
self.umount_file_sorting();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_RADIO_SORTING, Msg::OnSubmit(Payload::Unsigned(mode))) => {
|
(COMPONENT_RADIO_SORTING, Msg::OnChange(Payload::One(Value::Usize(mode)))) => {
|
||||||
// Get sorting mode
|
// Get sorting mode
|
||||||
let sorting: FileSorting = match mode {
|
let sorting: FileSorting = match mode {
|
||||||
1 => FileSorting::ByModifyTime,
|
1 => FileSorting::ByModifyTime,
|
||||||
@@ -674,14 +612,15 @@ impl FileTransferActivity {
|
|||||||
3 => FileSorting::BySize,
|
3 => FileSorting::BySize,
|
||||||
_ => FileSorting::ByName,
|
_ => FileSorting::ByName,
|
||||||
};
|
};
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.local.sort_by(sorting),
|
FileExplorerTab::Local => self.local_mut().sort_by(sorting),
|
||||||
FileExplorerTab::Remote => self.remote.sort_by(sorting),
|
FileExplorerTab::Remote => self.remote_mut().sort_by(sorting),
|
||||||
_ => panic!("Found result doesn't support SORTING"),
|
_ => panic!("Found result doesn't support SORTING"),
|
||||||
}
|
}
|
||||||
self.umount_file_sorting();
|
// Update status bar
|
||||||
|
self.refresh_status_bar();
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.update_local_filelist(),
|
FileExplorerTab::Local => self.update_local_filelist(),
|
||||||
FileExplorerTab::Remote => self.update_remote_filelist(),
|
FileExplorerTab::Remote => self.update_remote_filelist(),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -703,9 +642,9 @@ impl FileTransferActivity {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
// -- progress bar
|
// -- progress bar
|
||||||
(COMPONENT_PROGRESS_BAR, &MSG_KEY_CTRL_C) => {
|
(COMPONENT_PROGRESS_BAR_PARTIAL, &MSG_KEY_CTRL_C) => {
|
||||||
// Set transfer aborted to True
|
// Set transfer aborted to True
|
||||||
self.transfer.aborted = true;
|
self.transfer.abort();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
// -- fallback
|
// -- fallback
|
||||||
@@ -718,11 +657,7 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Update local file list
|
/// Update local file list
|
||||||
pub(super) fn update_local_filelist(&mut self) -> Option<(String, Msg)> {
|
pub(super) fn update_local_filelist(&mut self) -> Option<(String, Msg)> {
|
||||||
match self
|
match self.view.get_props(super::COMPONENT_EXPLORER_LOCAL) {
|
||||||
.view
|
|
||||||
.get_props(super::COMPONENT_EXPLORER_LOCAL)
|
|
||||||
.as_mut()
|
|
||||||
{
|
|
||||||
Some(props) => {
|
Some(props) => {
|
||||||
// Get width
|
// Get width
|
||||||
let width: usize = self
|
let width: usize = self
|
||||||
@@ -744,20 +679,20 @@ impl FileTransferActivity {
|
|||||||
"{}:{} ",
|
"{}:{} ",
|
||||||
hostname,
|
hostname,
|
||||||
FileTransferActivity::elide_wrkdir_path(
|
FileTransferActivity::elide_wrkdir_path(
|
||||||
self.local.wrkdir.as_path(),
|
self.local().wrkdir.as_path(),
|
||||||
hostname.as_str(),
|
hostname.as_str(),
|
||||||
width
|
width
|
||||||
)
|
)
|
||||||
.display()
|
.display()
|
||||||
);
|
);
|
||||||
let files: Vec<TextSpan> = self
|
let files: Vec<String> = self
|
||||||
.local
|
.local()
|
||||||
.iter_files()
|
.iter_files()
|
||||||
.map(|x: &FsEntry| TextSpan::from(self.local.fmt_file(x)))
|
.map(|x: &FsEntry| self.local().fmt_file(x))
|
||||||
.collect();
|
.collect();
|
||||||
// Update
|
// Update
|
||||||
let props = props
|
let props = FileListPropsBuilder::from(props)
|
||||||
.with_texts(TextParts::new(Some(hostname), Some(files)))
|
.with_files(Some(hostname), files)
|
||||||
.build();
|
.build();
|
||||||
// Update
|
// Update
|
||||||
self.view.update(super::COMPONENT_EXPLORER_LOCAL, props)
|
self.view.update(super::COMPONENT_EXPLORER_LOCAL, props)
|
||||||
@@ -770,11 +705,7 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Update remote file list
|
/// Update remote file list
|
||||||
pub(super) fn update_remote_filelist(&mut self) -> Option<(String, Msg)> {
|
pub(super) fn update_remote_filelist(&mut self) -> Option<(String, Msg)> {
|
||||||
match self
|
match self.view.get_props(super::COMPONENT_EXPLORER_REMOTE) {
|
||||||
.view
|
|
||||||
.get_props(super::COMPONENT_EXPLORER_REMOTE)
|
|
||||||
.as_mut()
|
|
||||||
{
|
|
||||||
Some(props) => {
|
Some(props) => {
|
||||||
// Get width
|
// Get width
|
||||||
let width: usize = self
|
let width: usize = self
|
||||||
@@ -789,20 +720,20 @@ impl FileTransferActivity {
|
|||||||
"{}:{} ",
|
"{}:{} ",
|
||||||
params.address,
|
params.address,
|
||||||
FileTransferActivity::elide_wrkdir_path(
|
FileTransferActivity::elide_wrkdir_path(
|
||||||
self.remote.wrkdir.as_path(),
|
self.remote().wrkdir.as_path(),
|
||||||
params.address.as_str(),
|
params.address.as_str(),
|
||||||
width
|
width
|
||||||
)
|
)
|
||||||
.display()
|
.display()
|
||||||
);
|
);
|
||||||
let files: Vec<TextSpan> = self
|
let files: Vec<String> = self
|
||||||
.remote
|
.remote()
|
||||||
.iter_files()
|
.iter_files()
|
||||||
.map(|x: &FsEntry| TextSpan::from(self.remote.fmt_file(x)))
|
.map(|x: &FsEntry| self.remote().fmt_file(x))
|
||||||
.collect();
|
.collect();
|
||||||
// Update
|
// Update
|
||||||
let props = props
|
let props = FileListPropsBuilder::from(props)
|
||||||
.with_texts(TextParts::new(Some(hostname), Some(files)))
|
.with_files(Some(hostname), files)
|
||||||
.build();
|
.build();
|
||||||
self.view.update(super::COMPONENT_EXPLORER_REMOTE, props)
|
self.view.update(super::COMPONENT_EXPLORER_REMOTE, props)
|
||||||
}
|
}
|
||||||
@@ -814,21 +745,11 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Update log box
|
/// Update log box
|
||||||
pub(super) fn update_logbox(&mut self) -> Option<(String, Msg)> {
|
pub(super) fn update_logbox(&mut self) -> Option<(String, Msg)> {
|
||||||
match self.view.get_props(super::COMPONENT_LOG_BOX).as_mut() {
|
match self.view.get_props(super::COMPONENT_LOG_BOX) {
|
||||||
Some(props) => {
|
Some(props) => {
|
||||||
// Get width
|
|
||||||
let width: usize = self
|
|
||||||
.context
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.store
|
|
||||||
.get_unsigned(super::STORAGE_LOGBOX_WIDTH)
|
|
||||||
.unwrap_or(256);
|
|
||||||
// Make log entries
|
// Make log entries
|
||||||
let mut table: TableBuilder = TableBuilder::default();
|
let mut table: TableBuilder = TableBuilder::default();
|
||||||
for (idx, record) in self.log_records.iter().enumerate() {
|
for (idx, record) in self.log_records.iter().enumerate() {
|
||||||
// Split rows by width NOTE: -37 'cause log prefix -3 cause of log line cursor
|
|
||||||
let record_rows = textwrap::wrap(record.msg.as_str(), (width as usize) - 40);
|
|
||||||
// Add row if not first row
|
// Add row if not first row
|
||||||
if idx > 0 {
|
if idx > 0 {
|
||||||
table.add_row();
|
table.add_row();
|
||||||
@@ -838,46 +759,33 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Warn => Color::Yellow,
|
LogLevel::Warn => Color::Yellow,
|
||||||
LogLevel::Info => Color::Green,
|
LogLevel::Info => Color::Green,
|
||||||
};
|
};
|
||||||
for (idx, row) in record_rows.iter().enumerate() {
|
table
|
||||||
match idx {
|
.add_col(TextSpan::from(format!(
|
||||||
0 => {
|
"{}",
|
||||||
// First row
|
record.time.format("%Y-%m-%dT%H:%M:%S%Z")
|
||||||
table
|
)))
|
||||||
.add_col(TextSpan::from(format!(
|
.add_col(TextSpan::from(" ["))
|
||||||
"{}",
|
.add_col(
|
||||||
record.time.format("%Y-%m-%dT%H:%M:%S%Z")
|
TextSpanBuilder::new(
|
||||||
)))
|
format!(
|
||||||
.add_col(TextSpan::from(" ["))
|
"{:5}",
|
||||||
.add_col(
|
match record.level {
|
||||||
TextSpanBuilder::new(
|
LogLevel::Error => "ERROR",
|
||||||
format!(
|
LogLevel::Warn => "WARN",
|
||||||
"{:5}",
|
LogLevel::Info => "INFO",
|
||||||
match record.level {
|
}
|
||||||
LogLevel::Error => "ERROR",
|
)
|
||||||
LogLevel::Warn => "WARN",
|
.as_str(),
|
||||||
LogLevel::Info => "INFO",
|
)
|
||||||
}
|
.with_foreground(fg)
|
||||||
)
|
.build(),
|
||||||
.as_str(),
|
)
|
||||||
)
|
.add_col(TextSpan::from("]: "))
|
||||||
.with_foreground(fg)
|
.add_col(TextSpan::from(record.msg.as_ref()));
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.add_col(TextSpan::from("]: "))
|
|
||||||
.add_col(TextSpan::from(row.as_ref()));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
table.add_col(TextSpan::from(textwrap::indent(
|
|
||||||
row.as_ref(),
|
|
||||||
" ",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let table = table.build();
|
let table = table.build();
|
||||||
let props = props
|
let props = LogboxPropsBuilder::from(props)
|
||||||
.with_texts(TextParts::table(Some(String::from("Log")), table))
|
.with_log(Some(String::from("Log")), table)
|
||||||
.build();
|
.build();
|
||||||
self.view.update(super::COMPONENT_LOG_BOX, props)
|
self.view.update(super::COMPONENT_LOG_BOX, props)
|
||||||
}
|
}
|
||||||
@@ -885,34 +793,22 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn update_progress_bar(&mut self, text: String) -> Option<(String, Msg)> {
|
pub(super) fn update_progress_bar(&mut self, filename: String) -> Option<(String, Msg)> {
|
||||||
match self.view.get_props(COMPONENT_PROGRESS_BAR).as_mut() {
|
if let Some(props) = self.view.get_props(COMPONENT_PROGRESS_BAR_FULL) {
|
||||||
|
let root_name: String = props.texts.title.as_deref().unwrap_or("").to_string();
|
||||||
|
let props = ProgressBarPropsBuilder::from(props)
|
||||||
|
.with_texts(Some(root_name), self.transfer.full.to_string())
|
||||||
|
.with_progress(self.transfer.full.calc_progress())
|
||||||
|
.build();
|
||||||
|
let _ = self.view.update(COMPONENT_PROGRESS_BAR_FULL, props);
|
||||||
|
}
|
||||||
|
match self.view.get_props(COMPONENT_PROGRESS_BAR_PARTIAL) {
|
||||||
Some(props) => {
|
Some(props) => {
|
||||||
// Calculate ETA
|
let props = ProgressBarPropsBuilder::from(props)
|
||||||
let elapsed_secs: u64 = self.transfer.started.elapsed().as_secs();
|
.with_texts(Some(filename), self.transfer.partial.to_string())
|
||||||
let eta: String = match self.transfer.progress as u64 {
|
.with_progress(self.transfer.partial.calc_progress())
|
||||||
0 => String::from("--:--"), // NOTE: would divide by 0 :D
|
|
||||||
_ => {
|
|
||||||
let eta: u64 =
|
|
||||||
((elapsed_secs * 100) / (self.transfer.progress as u64)) - elapsed_secs;
|
|
||||||
format!("{:0width$}:{:0width$}", (eta / 60), (eta % 60), width = 2)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Calculate bytes/s
|
|
||||||
let label = format!(
|
|
||||||
"{:.2}% - ETA {} ({}/s)",
|
|
||||||
self.transfer.progress,
|
|
||||||
eta,
|
|
||||||
ByteSize(self.transfer.bytes_per_second())
|
|
||||||
);
|
|
||||||
let props = props
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(text),
|
|
||||||
Some(vec![TextSpan::from(label)]),
|
|
||||||
))
|
|
||||||
.with_value(PropValue::Float(self.transfer.progress / 100.0))
|
|
||||||
.build();
|
.build();
|
||||||
self.view.update(COMPONENT_PROGRESS_BAR, props)
|
self.view.update(COMPONENT_PROGRESS_BAR_PARTIAL, props)
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
@@ -923,32 +819,29 @@ impl FileTransferActivity {
|
|||||||
/// Finalize find process
|
/// Finalize find process
|
||||||
fn finalize_find(&mut self) {
|
fn finalize_find(&mut self) {
|
||||||
// Set found to none
|
// Set found to none
|
||||||
self.found = None;
|
self.browser.del_found();
|
||||||
// Restore tab
|
// Restore tab
|
||||||
self.tab = match self.tab {
|
self.browser.change_tab(match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal => FileExplorerTab::Local,
|
FileExplorerTab::FindLocal => FileExplorerTab::Local,
|
||||||
FileExplorerTab::FindRemote => FileExplorerTab::Remote,
|
FileExplorerTab::FindRemote => FileExplorerTab::Remote,
|
||||||
_ => FileExplorerTab::Local,
|
_ => FileExplorerTab::Local,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_find_list(&mut self) -> Option<(String, Msg)> {
|
fn update_find_list(&mut self) -> Option<(String, Msg)> {
|
||||||
match self.view.get_props(COMPONENT_EXPLORER_FIND).as_mut() {
|
match self.view.get_props(COMPONENT_EXPLORER_FIND) {
|
||||||
None => None,
|
None => None,
|
||||||
Some(props) => {
|
Some(props) => {
|
||||||
let props = props.build();
|
|
||||||
let title: String = props.texts.title.clone().unwrap_or_default();
|
let title: String = props.texts.title.clone().unwrap_or_default();
|
||||||
let mut props = PropsBuilder::from(props);
|
|
||||||
// Prepare files
|
// Prepare files
|
||||||
let file_texts: Vec<TextSpan> = self
|
let files: Vec<String> = self
|
||||||
.found
|
.found()
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter_files()
|
.iter_files()
|
||||||
.map(|x: &FsEntry| TextSpan::from(self.found.as_ref().unwrap().fmt_file(x)))
|
.map(|x: &FsEntry| self.found().unwrap().fmt_file(x))
|
||||||
.collect();
|
.collect();
|
||||||
let props = props
|
let props = FileListPropsBuilder::from(props)
|
||||||
.with_texts(TextParts::new(Some(title), Some(file_texts)))
|
.with_files(Some(title), files)
|
||||||
.build();
|
.build();
|
||||||
self.view.update(COMPONENT_EXPLORER_FIND, props)
|
self.view.update(COMPONENT_EXPLORER_FIND, props)
|
||||||
}
|
}
|
||||||
@@ -31,26 +31,32 @@ extern crate hostname;
|
|||||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||||
extern crate users;
|
extern crate users;
|
||||||
// locals
|
// locals
|
||||||
use super::{Context, FileExplorerTab, FileTransferActivity};
|
use super::{browser::FileExplorerTab, Context, FileTransferActivity};
|
||||||
use crate::fs::explorer::FileSorting;
|
use crate::fs::explorer::FileSorting;
|
||||||
use crate::fs::FsEntry;
|
use crate::fs::FsEntry;
|
||||||
use crate::ui::layout::components::{
|
use crate::ui::components::{
|
||||||
file_list::FileList, input::Input, logbox::LogBox, msgbox::MsgBox, progress_bar::ProgressBar,
|
file_list::{FileList, FileListPropsBuilder},
|
||||||
radio_group::RadioGroup, table::Table,
|
logbox::{LogBox, LogboxPropsBuilder},
|
||||||
|
msgbox::{MsgBox, MsgBoxPropsBuilder},
|
||||||
};
|
};
|
||||||
use crate::ui::layout::props::{
|
|
||||||
PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
|
|
||||||
};
|
|
||||||
use crate::ui::layout::utils::draw_area_in;
|
|
||||||
use crate::ui::store::Store;
|
use crate::ui::store::Store;
|
||||||
use crate::utils::fmt::fmt_time;
|
use crate::utils::fmt::fmt_time;
|
||||||
|
use crate::utils::ui::draw_area_in;
|
||||||
// Ext
|
// Ext
|
||||||
use bytesize::ByteSize;
|
use bytesize::ByteSize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tui::{
|
use tuirealm::components::{
|
||||||
|
input::{Input, InputPropsBuilder},
|
||||||
|
progress_bar::{ProgressBar, ProgressBarPropsBuilder},
|
||||||
|
radio::{Radio, RadioPropsBuilder},
|
||||||
|
span::{Span, SpanPropsBuilder},
|
||||||
|
table::{Table, TablePropsBuilder},
|
||||||
|
};
|
||||||
|
use tuirealm::props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder};
|
||||||
|
use tuirealm::tui::{
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
style::Color,
|
style::Color,
|
||||||
widgets::Clear,
|
widgets::{BorderType, Borders, Clear},
|
||||||
};
|
};
|
||||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||||
use users::{get_group_by_gid, get_user_by_uid};
|
use users::{get_group_by_gid, get_user_by_uid};
|
||||||
@@ -66,9 +72,10 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_EXPLORER_LOCAL,
|
super::COMPONENT_EXPLORER_LOCAL,
|
||||||
Box::new(FileList::new(
|
Box::new(FileList::new(
|
||||||
PropsBuilder::default()
|
FileListPropsBuilder::default()
|
||||||
.with_background(Color::Yellow)
|
.with_background(Color::Yellow)
|
||||||
.with_foreground(Color::Yellow)
|
.with_foreground(Color::Yellow)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Plain, Color::Yellow)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -76,9 +83,10 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_EXPLORER_REMOTE,
|
super::COMPONENT_EXPLORER_REMOTE,
|
||||||
Box::new(FileList::new(
|
Box::new(FileList::new(
|
||||||
PropsBuilder::default()
|
FileListPropsBuilder::default()
|
||||||
.with_background(Color::LightBlue)
|
.with_background(Color::LightBlue)
|
||||||
.with_foreground(Color::LightBlue)
|
.with_foreground(Color::LightBlue)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Plain, Color::LightBlue)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -86,12 +94,18 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_LOG_BOX,
|
super::COMPONENT_LOG_BOX,
|
||||||
Box::new(LogBox::new(
|
Box::new(LogBox::new(
|
||||||
PropsBuilder::default()
|
LogboxPropsBuilder::default()
|
||||||
.with_foreground(Color::LightGreen)
|
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||||
.bold()
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
// Mount status bar
|
||||||
|
self.view.mount(
|
||||||
|
super::COMPONENT_SPAN_STATUS_BAR,
|
||||||
|
Box::new(Span::new(SpanPropsBuilder::default().build())),
|
||||||
|
);
|
||||||
|
// Load process bar
|
||||||
|
self.refresh_status_bar();
|
||||||
// Update components
|
// Update components
|
||||||
let _ = self.update_local_filelist();
|
let _ = self.update_local_filelist();
|
||||||
let _ = self.update_remote_filelist();
|
let _ = self.update_remote_filelist();
|
||||||
@@ -125,16 +139,18 @@ impl FileTransferActivity {
|
|||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.split(chunks[0]);
|
.split(chunks[0]);
|
||||||
|
// Create log box chunks
|
||||||
|
let bottom_chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Length(1), Constraint::Length(10)].as_ref())
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.split(chunks[1]);
|
||||||
// If width is unset in the storage, set width
|
// If width is unset in the storage, set width
|
||||||
if !store.isset(super::STORAGE_EXPLORER_WIDTH) {
|
if !store.isset(super::STORAGE_EXPLORER_WIDTH) {
|
||||||
store.set_unsigned(super::STORAGE_EXPLORER_WIDTH, tabs_chunks[0].width as usize);
|
store.set_unsigned(super::STORAGE_EXPLORER_WIDTH, tabs_chunks[0].width as usize);
|
||||||
}
|
}
|
||||||
if !store.isset(super::STORAGE_LOGBOX_WIDTH) {
|
|
||||||
store.set_unsigned(super::STORAGE_LOGBOX_WIDTH, chunks[1].width as usize);
|
|
||||||
}
|
|
||||||
// Draw explorers
|
// Draw explorers
|
||||||
// @! Local explorer (Find or default)
|
// @! Local explorer (Find or default)
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal => {
|
FileExplorerTab::FindLocal => {
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[0])
|
.render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[0])
|
||||||
@@ -144,7 +160,7 @@ impl FileTransferActivity {
|
|||||||
.render(super::COMPONENT_EXPLORER_LOCAL, f, tabs_chunks[0]),
|
.render(super::COMPONENT_EXPLORER_LOCAL, f, tabs_chunks[0]),
|
||||||
}
|
}
|
||||||
// @! Remote explorer (Find or default)
|
// @! Remote explorer (Find or default)
|
||||||
match self.tab {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindRemote => {
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[1])
|
.render(super::COMPONENT_EXPLORER_FIND, f, tabs_chunks[1])
|
||||||
@@ -153,99 +169,115 @@ impl FileTransferActivity {
|
|||||||
.view
|
.view
|
||||||
.render(super::COMPONENT_EXPLORER_REMOTE, f, tabs_chunks[1]),
|
.render(super::COMPONENT_EXPLORER_REMOTE, f, tabs_chunks[1]),
|
||||||
}
|
}
|
||||||
// Draw log box
|
// Draw log box and status bar
|
||||||
self.view.render(super::COMPONENT_LOG_BOX, f, chunks[1]);
|
self.view
|
||||||
|
.render(super::COMPONENT_LOG_BOX, f, bottom_chunks[1]);
|
||||||
|
self.view
|
||||||
|
.render(super::COMPONENT_SPAN_STATUS_BAR, f, bottom_chunks[0]);
|
||||||
// @! Draw popups
|
// @! Draw popups
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_COPY) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_COPY) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 40, 10);
|
let popup = draw_area_in(f.size(), 40, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_INPUT_COPY, f, popup);
|
self.view.render(super::COMPONENT_INPUT_COPY, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_FIND) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_FIND) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 40, 10);
|
let popup = draw_area_in(f.size(), 40, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_INPUT_FIND, f, popup);
|
self.view.render(super::COMPONENT_INPUT_FIND, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_GOTO) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_GOTO) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 40, 10);
|
let popup = draw_area_in(f.size(), 40, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_INPUT_GOTO, f, popup);
|
self.view.render(super::COMPONENT_INPUT_GOTO, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_MKDIR) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_MKDIR) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 40, 10);
|
let popup = draw_area_in(f.size(), 40, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_INPUT_MKDIR, f, popup);
|
self.view.render(super::COMPONENT_INPUT_MKDIR, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_NEWFILE) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_NEWFILE) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 40, 10);
|
let popup = draw_area_in(f.size(), 40, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_INPUT_NEWFILE, f, popup);
|
self.view.render(super::COMPONENT_INPUT_NEWFILE, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 40, 10);
|
let popup = draw_area_in(f.size(), 40, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_INPUT_RENAME, f, popup);
|
self.view.render(super::COMPONENT_INPUT_RENAME, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_SAVEAS) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SAVEAS) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 40, 10);
|
let popup = draw_area_in(f.size(), 40, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_INPUT_SAVEAS, f, popup);
|
self.view.render(super::COMPONENT_INPUT_SAVEAS, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_EXEC) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_EXEC) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 40, 10);
|
let popup = draw_area_in(f.size(), 40, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_INPUT_EXEC, f, popup);
|
self.view.render(super::COMPONENT_INPUT_EXEC, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_LIST_FILEINFO) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_FILEINFO) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 50, 50);
|
let popup = draw_area_in(f.size(), 50, 50);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_LIST_FILEINFO, f, popup);
|
self.view.render(super::COMPONENT_LIST_FILEINFO, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR_PARTIAL) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 40, 10);
|
let popup = draw_area_in(f.size(), 50, 20);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_PROGRESS_BAR, f, popup);
|
let popup_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Percentage(50), // Full
|
||||||
|
Constraint::Percentage(50), // Partial
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(popup);
|
||||||
|
self.view
|
||||||
|
.render(super::COMPONENT_PROGRESS_BAR_FULL, f, popup_chunks[0]);
|
||||||
|
self.view
|
||||||
|
.render(super::COMPONENT_PROGRESS_BAR_PARTIAL, f, popup_chunks[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DELETE) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DELETE) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 30, 10);
|
let popup = draw_area_in(f.size(), 30, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_RADIO_DELETE, f, popup);
|
self.view.render(super::COMPONENT_RADIO_DELETE, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 30, 10);
|
let popup = draw_area_in(f.size(), 30, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
@@ -253,48 +285,48 @@ impl FileTransferActivity {
|
|||||||
.render(super::COMPONENT_RADIO_DISCONNECT, f, popup);
|
.render(super::COMPONENT_RADIO_DISCONNECT, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 30, 10);
|
let popup = draw_area_in(f.size(), 30, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_SORTING) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SORTING) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 50, 10);
|
let popup = draw_area_in(f.size(), 50, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_RADIO_SORTING, f, popup);
|
self.view.render(super::COMPONENT_RADIO_SORTING, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 50, 10);
|
let popup = draw_area_in(f.size(), 50, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_FATAL) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_FATAL) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 50, 10);
|
let popup = draw_area_in(f.size(), 50, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_TEXT_FATAL, f, popup);
|
self.view.render(super::COMPONENT_TEXT_FATAL, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 50, 10);
|
let popup = draw_area_in(f.size(), 50, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_TEXT_WAIT, f, popup);
|
self.view.render(super::COMPONENT_TEXT_WAIT, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 50, 80);
|
let popup = draw_area_in(f.size(), 50, 80);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
@@ -316,10 +348,11 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_ERROR,
|
super::COMPONENT_TEXT_ERROR,
|
||||||
Box::new(MsgBox::new(
|
Box::new(MsgBox::new(
|
||||||
PropsBuilder::default()
|
MsgBoxPropsBuilder::default()
|
||||||
.with_foreground(Color::Red)
|
.with_foreground(Color::Red)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
|
||||||
.bold()
|
.bold()
|
||||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
|
.with_texts(None, vec![TextSpan::from(text)])
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -339,10 +372,11 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_FATAL,
|
super::COMPONENT_TEXT_FATAL,
|
||||||
Box::new(MsgBox::new(
|
Box::new(MsgBox::new(
|
||||||
PropsBuilder::default()
|
MsgBoxPropsBuilder::default()
|
||||||
.with_foreground(Color::Red)
|
.with_foreground(Color::Red)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
|
||||||
.bold()
|
.bold()
|
||||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
|
.with_texts(None, vec![TextSpan::from(text)])
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -355,10 +389,11 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_WAIT,
|
super::COMPONENT_TEXT_WAIT,
|
||||||
Box::new(MsgBox::new(
|
Box::new(MsgBox::new(
|
||||||
PropsBuilder::default()
|
MsgBoxPropsBuilder::default()
|
||||||
.with_foreground(Color::White)
|
.with_foreground(Color::White)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
.bold()
|
.bold()
|
||||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
|
.with_texts(None, vec![TextSpan::from(text)])
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -377,14 +412,15 @@ impl FileTransferActivity {
|
|||||||
// Protocol
|
// Protocol
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_QUIT,
|
super::COMPONENT_RADIO_QUIT,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::Yellow)
|
.with_color(Color::Yellow)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Are you sure you want to quit?")),
|
Some(String::from("Are you sure you want to quit?")),
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
vec![String::from("Yes"), String::from("No")],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -405,14 +441,15 @@ impl FileTransferActivity {
|
|||||||
// Protocol
|
// Protocol
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_DISCONNECT,
|
super::COMPONENT_RADIO_DISCONNECT,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::Yellow)
|
.with_color(Color::Yellow)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Are you sure you want to disconnect?")),
|
Some(String::from("Are you sure you want to disconnect?")),
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
vec![String::from("Yes"), String::from("No")],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -430,11 +467,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_COPY,
|
super::COMPONENT_INPUT_COPY,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
Some(String::from("Insert destination name")),
|
.with_label(String::from("Copy file(s) to..."))
|
||||||
None,
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -449,8 +484,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_EXEC,
|
super::COMPONENT_INPUT_EXEC,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(Some(String::from("Execute command")), None))
|
.with_borders(Borders::ALL, BorderType::Plain, Color::White)
|
||||||
|
.with_label(String::from("Execute command"))
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -463,7 +499,7 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
pub(super) fn mount_find(&mut self, search: &str) {
|
pub(super) fn mount_find(&mut self, search: &str) {
|
||||||
// Get color
|
// Get color
|
||||||
let color: Color = match self.tab {
|
let color: Color = match self.browser.tab() {
|
||||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => Color::Yellow,
|
FileExplorerTab::Local | FileExplorerTab::FindLocal => Color::Yellow,
|
||||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => Color::LightBlue,
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => Color::LightBlue,
|
||||||
};
|
};
|
||||||
@@ -471,11 +507,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_EXPLORER_FIND,
|
super::COMPONENT_EXPLORER_FIND,
|
||||||
Box::new(FileList::new(
|
Box::new(FileList::new(
|
||||||
PropsBuilder::default()
|
FileListPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.with_files(Some(format!("Search results for \"{}\"", search)), vec![])
|
||||||
Some(format!("Search results for \"{}\"", search)),
|
.with_borders(Borders::ALL, BorderType::Plain, color)
|
||||||
Some(vec![]),
|
|
||||||
))
|
|
||||||
.with_background(color)
|
.with_background(color)
|
||||||
.with_foreground(color)
|
.with_foreground(color)
|
||||||
.build(),
|
.build(),
|
||||||
@@ -493,11 +527,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_FIND,
|
super::COMPONENT_INPUT_FIND,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
Some(String::from("Search files by name")),
|
.with_label(String::from("Search files by name"))
|
||||||
None,
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -514,11 +546,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_GOTO,
|
super::COMPONENT_INPUT_GOTO,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
Some(String::from("Change working directory")),
|
.with_label(String::from("Change working directory"))
|
||||||
None,
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -533,11 +563,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_MKDIR,
|
super::COMPONENT_INPUT_MKDIR,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
Some(String::from("Insert directory name")),
|
.with_label(String::from("Insert directory name"))
|
||||||
None,
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -552,8 +580,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_NEWFILE,
|
super::COMPONENT_INPUT_NEWFILE,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(Some(String::from("New file name")), None))
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
|
.with_label(String::from("New file name"))
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -568,8 +597,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_RENAME,
|
super::COMPONENT_INPUT_RENAME,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(Some(String::from("Insert new name")), None))
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
|
.with_label(String::from("Move file(s) to..."))
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -584,8 +614,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_SAVEAS,
|
super::COMPONENT_INPUT_SAVEAS,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(Some(String::from("Save as...")), None))
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
|
.with_label(String::from("Save as..."))
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -596,28 +627,49 @@ impl FileTransferActivity {
|
|||||||
self.view.umount(super::COMPONENT_INPUT_SAVEAS);
|
self.view.umount(super::COMPONENT_INPUT_SAVEAS);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_progress_bar(&mut self) {
|
pub(super) fn mount_progress_bar(&mut self, root_name: String) {
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_PROGRESS_BAR,
|
super::COMPONENT_PROGRESS_BAR_FULL,
|
||||||
Box::new(ProgressBar::new(
|
Box::new(ProgressBar::new(
|
||||||
PropsBuilder::default()
|
ProgressBarPropsBuilder::default()
|
||||||
.with_foreground(Color::LightGreen)
|
.with_progbar_color(Color::Green)
|
||||||
.with_background(Color::Black)
|
.with_background(Color::Black)
|
||||||
.with_texts(TextParts::new(Some(String::from("Please wait")), None))
|
.with_borders(
|
||||||
|
Borders::TOP | Borders::RIGHT | Borders::LEFT,
|
||||||
|
BorderType::Rounded,
|
||||||
|
Color::Reset,
|
||||||
|
)
|
||||||
|
.with_texts(Some(root_name), String::new())
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
self.view.active(super::COMPONENT_PROGRESS_BAR);
|
self.view.mount(
|
||||||
|
super::COMPONENT_PROGRESS_BAR_PARTIAL,
|
||||||
|
Box::new(ProgressBar::new(
|
||||||
|
ProgressBarPropsBuilder::default()
|
||||||
|
.with_progbar_color(Color::Green)
|
||||||
|
.with_background(Color::Black)
|
||||||
|
.with_borders(
|
||||||
|
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
|
||||||
|
BorderType::Rounded,
|
||||||
|
Color::Reset,
|
||||||
|
)
|
||||||
|
.with_texts(Some(String::from("Please wait")), String::new())
|
||||||
|
.build(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
self.view.active(super::COMPONENT_PROGRESS_BAR_PARTIAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn umount_progress_bar(&mut self) {
|
pub(super) fn umount_progress_bar(&mut self) {
|
||||||
self.view.umount(super::COMPONENT_PROGRESS_BAR);
|
self.view.umount(super::COMPONENT_PROGRESS_BAR_PARTIAL);
|
||||||
|
self.view.umount(super::COMPONENT_PROGRESS_BAR_FULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_file_sorting(&mut self) {
|
pub(super) fn mount_file_sorting(&mut self) {
|
||||||
let sorting: FileSorting = match self.tab {
|
let sorting: FileSorting = match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.local.get_file_sorting(),
|
FileExplorerTab::Local => self.local().get_file_sorting(),
|
||||||
FileExplorerTab::Remote => self.remote.get_file_sorting(),
|
FileExplorerTab::Remote => self.remote().get_file_sorting(),
|
||||||
_ => panic!("You can't mount file sorting when in found result"),
|
_ => panic!("You can't mount file sorting when in found result"),
|
||||||
};
|
};
|
||||||
let index: usize = match sorting {
|
let index: usize = match sorting {
|
||||||
@@ -628,20 +680,21 @@ impl FileTransferActivity {
|
|||||||
};
|
};
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_SORTING,
|
super::COMPONENT_RADIO_SORTING,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::LightMagenta)
|
.with_color(Color::LightMagenta)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Sort files by")),
|
Some(String::from("Sort files by")),
|
||||||
Some(vec![
|
vec![
|
||||||
TextSpan::from("Name"),
|
String::from("Name"),
|
||||||
TextSpan::from("Modify time"),
|
String::from("Modify time"),
|
||||||
TextSpan::from("Creation time"),
|
String::from("Creation time"),
|
||||||
TextSpan::from("Size"),
|
String::from("Size"),
|
||||||
]),
|
],
|
||||||
))
|
)
|
||||||
.with_value(PropValue::Unsigned(index))
|
.with_value(index)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -655,15 +708,16 @@ impl FileTransferActivity {
|
|||||||
pub(super) fn mount_radio_delete(&mut self) {
|
pub(super) fn mount_radio_delete(&mut self) {
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_DELETE,
|
super::COMPONENT_RADIO_DELETE,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::Red)
|
.with_color(Color::Red)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Plain, Color::Red)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Delete file")),
|
Some(String::from("Delete file")),
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
vec![String::from("Yes"), String::from("No")],
|
||||||
))
|
)
|
||||||
.with_value(PropValue::Unsigned(1))
|
.with_value(1)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -772,11 +826,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_LIST_FILEINFO,
|
super::COMPONENT_LIST_FILEINFO,
|
||||||
Box::new(Table::new(
|
Box::new(Table::new(
|
||||||
PropsBuilder::default()
|
TablePropsBuilder::default()
|
||||||
.with_texts(TextParts::table(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
Some(file.get_name().to_string()),
|
.with_table(Some(file.get_name().to_string()), texts.build())
|
||||||
texts.build(),
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -787,6 +839,41 @@ impl FileTransferActivity {
|
|||||||
self.view.umount(super::COMPONENT_LIST_FILEINFO);
|
self.view.umount(super::COMPONENT_LIST_FILEINFO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn refresh_status_bar(&mut self) {
|
||||||
|
let bar_spans: Vec<TextSpan> = vec![
|
||||||
|
TextSpanBuilder::new("Synchronized Browsing: ")
|
||||||
|
.with_foreground(Color::LightGreen)
|
||||||
|
.build(),
|
||||||
|
TextSpanBuilder::new(match self.browser.sync_browsing {
|
||||||
|
true => "ON ",
|
||||||
|
false => "OFF",
|
||||||
|
})
|
||||||
|
.with_foreground(Color::LightGreen)
|
||||||
|
.reversed()
|
||||||
|
.build(),
|
||||||
|
TextSpanBuilder::new(" Localhost file sorting: ")
|
||||||
|
.with_foreground(Color::LightYellow)
|
||||||
|
.build(),
|
||||||
|
TextSpanBuilder::new(Self::get_file_sorting_str(self.local().get_file_sorting()))
|
||||||
|
.with_foreground(Color::LightYellow)
|
||||||
|
.reversed()
|
||||||
|
.build(),
|
||||||
|
TextSpanBuilder::new(" Remote host file sorting: ")
|
||||||
|
.with_foreground(Color::LightBlue)
|
||||||
|
.build(),
|
||||||
|
TextSpanBuilder::new(Self::get_file_sorting_str(self.remote().get_file_sorting()))
|
||||||
|
.with_foreground(Color::LightBlue)
|
||||||
|
.reversed()
|
||||||
|
.build(),
|
||||||
|
];
|
||||||
|
if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR) {
|
||||||
|
self.view.update(
|
||||||
|
super::COMPONENT_SPAN_STATUS_BAR,
|
||||||
|
SpanPropsBuilder::from(props).with_spans(bar_spans).build(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// ### mount_help
|
/// ### mount_help
|
||||||
///
|
///
|
||||||
/// Mount help
|
/// Mount help
|
||||||
@@ -794,8 +881,9 @@ impl FileTransferActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_HELP,
|
super::COMPONENT_TEXT_HELP,
|
||||||
Box::new(Table::new(
|
Box::new(Table::new(
|
||||||
PropsBuilder::default()
|
TablePropsBuilder::default()
|
||||||
.with_texts(TextParts::table(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
|
.with_table(
|
||||||
Some(String::from("Help")),
|
Some(String::from("Help")),
|
||||||
TableBuilder::default()
|
TableBuilder::default()
|
||||||
.add_col(
|
.add_col(
|
||||||
@@ -920,6 +1008,14 @@ impl FileTransferActivity {
|
|||||||
)
|
)
|
||||||
.add_col(TextSpan::from(" Reload directory content"))
|
.add_col(TextSpan::from(" Reload directory content"))
|
||||||
.add_row()
|
.add_row()
|
||||||
|
.add_col(
|
||||||
|
TextSpanBuilder::new("<M>")
|
||||||
|
.bold()
|
||||||
|
.with_foreground(Color::Cyan)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.add_col(TextSpan::from(" Select file"))
|
||||||
|
.add_row()
|
||||||
.add_col(
|
.add_col(
|
||||||
TextSpanBuilder::new("<N>")
|
TextSpanBuilder::new("<N>")
|
||||||
.bold()
|
.bold()
|
||||||
@@ -968,6 +1064,22 @@ impl FileTransferActivity {
|
|||||||
)
|
)
|
||||||
.add_col(TextSpan::from(" Go to parent directory"))
|
.add_col(TextSpan::from(" Go to parent directory"))
|
||||||
.add_row()
|
.add_row()
|
||||||
|
.add_col(
|
||||||
|
TextSpanBuilder::new("<X>")
|
||||||
|
.bold()
|
||||||
|
.with_foreground(Color::Cyan)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.add_col(TextSpan::from(" Execute shell command"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(
|
||||||
|
TextSpanBuilder::new("<Y>")
|
||||||
|
.bold()
|
||||||
|
.with_foreground(Color::Cyan)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.add_col(TextSpan::from(" Toggle synchronized browsing"))
|
||||||
|
.add_row()
|
||||||
.add_col(
|
.add_col(
|
||||||
TextSpanBuilder::new("<DEL|E>")
|
TextSpanBuilder::new("<DEL|E>")
|
||||||
.bold()
|
.bold()
|
||||||
@@ -976,6 +1088,14 @@ impl FileTransferActivity {
|
|||||||
)
|
)
|
||||||
.add_col(TextSpan::from(" Delete selected file"))
|
.add_col(TextSpan::from(" Delete selected file"))
|
||||||
.add_row()
|
.add_row()
|
||||||
|
.add_col(
|
||||||
|
TextSpanBuilder::new("<CTRL+A>")
|
||||||
|
.bold()
|
||||||
|
.with_foreground(Color::Cyan)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.add_col(TextSpan::from(" Select all files"))
|
||||||
|
.add_row()
|
||||||
.add_col(
|
.add_col(
|
||||||
TextSpanBuilder::new("<CTRL+C>")
|
TextSpanBuilder::new("<CTRL+C>")
|
||||||
.bold()
|
.bold()
|
||||||
@@ -984,7 +1104,7 @@ impl FileTransferActivity {
|
|||||||
)
|
)
|
||||||
.add_col(TextSpan::from(" Interrupt file transfer"))
|
.add_col(TextSpan::from(" Interrupt file transfer"))
|
||||||
.build(),
|
.build(),
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -995,4 +1115,13 @@ impl FileTransferActivity {
|
|||||||
pub(super) fn umount_help(&mut self) {
|
pub(super) fn umount_help(&mut self) {
|
||||||
self.view.umount(super::COMPONENT_TEXT_HELP);
|
self.view.umount(super::COMPONENT_TEXT_HELP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_file_sorting_str(mode: FileSorting) -> &'static str {
|
||||||
|
match mode {
|
||||||
|
FileSorting::ByName => "By name",
|
||||||
|
FileSorting::ByCreationTime => "By creation time",
|
||||||
|
FileSorting::ByModifyTime => "By modify time",
|
||||||
|
FileSorting::BySize => "By size",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,640 +0,0 @@
|
|||||||
//! ## FileTransferActivity
|
|
||||||
//!
|
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// locals
|
|
||||||
use super::{FileExplorerTab, FileTransferActivity, FsEntry, LogLevel};
|
|
||||||
use crate::ui::layout::Payload;
|
|
||||||
// externals
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
impl FileTransferActivity {
|
|
||||||
/// ### action_change_local_dir
|
|
||||||
///
|
|
||||||
/// Change local directory reading value from input
|
|
||||||
pub(super) fn action_change_local_dir(&mut self, input: String) {
|
|
||||||
let dir_path: PathBuf = PathBuf::from(input.as_str());
|
|
||||||
let abs_dir_path: PathBuf = match dir_path.is_relative() {
|
|
||||||
true => {
|
|
||||||
let mut d: PathBuf = self.local.wrkdir.clone();
|
|
||||||
d.push(dir_path);
|
|
||||||
d
|
|
||||||
}
|
|
||||||
false => dir_path,
|
|
||||||
};
|
|
||||||
self.local_changedir(abs_dir_path.as_path(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### action_change_remote_dir
|
|
||||||
///
|
|
||||||
/// Change remote directory reading value from input
|
|
||||||
pub(super) fn action_change_remote_dir(&mut self, input: String) {
|
|
||||||
let dir_path: PathBuf = PathBuf::from(input.as_str());
|
|
||||||
let abs_dir_path: PathBuf = match dir_path.is_relative() {
|
|
||||||
true => {
|
|
||||||
let mut wrkdir: PathBuf = self.remote.wrkdir.clone();
|
|
||||||
wrkdir.push(dir_path);
|
|
||||||
wrkdir
|
|
||||||
}
|
|
||||||
false => dir_path,
|
|
||||||
};
|
|
||||||
self.remote_changedir(abs_dir_path.as_path(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### action_local_copy
|
|
||||||
///
|
|
||||||
/// Copy file on local
|
|
||||||
pub(super) fn action_local_copy(&mut self, input: String) {
|
|
||||||
if let Some(idx) = self.get_local_file_idx() {
|
|
||||||
let dest_path: PathBuf = PathBuf::from(input);
|
|
||||||
let entry: FsEntry = self.local.get(idx).unwrap().clone();
|
|
||||||
if let Some(ctx) = self.context.as_mut() {
|
|
||||||
match ctx.local.copy(&entry, dest_path.as_path()) {
|
|
||||||
Ok(_) => {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!(
|
|
||||||
"Copied \"{}\" to \"{}\"",
|
|
||||||
entry.get_abs_path().display(),
|
|
||||||
dest_path.display()
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
// Reload entries
|
|
||||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
|
||||||
self.local_scan(wrkdir.as_path());
|
|
||||||
}
|
|
||||||
Err(err) => self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not copy \"{}\" to \"{}\": {}",
|
|
||||||
entry.get_abs_path().display(),
|
|
||||||
dest_path.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### action_remote_copy
|
|
||||||
///
|
|
||||||
/// Copy file on remote
|
|
||||||
pub(super) fn action_remote_copy(&mut self, input: String) {
|
|
||||||
if let Some(idx) = self.get_remote_file_idx() {
|
|
||||||
let dest_path: PathBuf = PathBuf::from(input);
|
|
||||||
let entry: FsEntry = self.remote.get(idx).unwrap().clone();
|
|
||||||
match self.client.as_mut().copy(&entry, dest_path.as_path()) {
|
|
||||||
Ok(_) => {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!(
|
|
||||||
"Copied \"{}\" to \"{}\"",
|
|
||||||
entry.get_abs_path().display(),
|
|
||||||
dest_path.display()
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
self.reload_remote_dir();
|
|
||||||
}
|
|
||||||
Err(err) => self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not copy \"{}\" to \"{}\": {}",
|
|
||||||
entry.get_abs_path().display(),
|
|
||||||
dest_path.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_local_mkdir(&mut self, input: String) {
|
|
||||||
match self
|
|
||||||
.context
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.local
|
|
||||||
.mkdir(PathBuf::from(input.as_str()).as_path())
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
// Reload files
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Created directory \"{}\"", input).as_ref(),
|
|
||||||
);
|
|
||||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
|
||||||
self.local_scan(wrkdir.as_path());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
// Report err
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not create directory \"{}\": {}", input, err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(super) fn action_remote_mkdir(&mut self, input: String) {
|
|
||||||
match self
|
|
||||||
.client
|
|
||||||
.as_mut()
|
|
||||||
.mkdir(PathBuf::from(input.as_str()).as_path())
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
// Reload files
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Created directory \"{}\"", input).as_ref(),
|
|
||||||
);
|
|
||||||
self.reload_remote_dir();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
// Report err
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not create directory \"{}\": {}", input, err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_local_rename(&mut self, input: String) {
|
|
||||||
let entry: Option<FsEntry> = self.get_local_file_entry().cloned();
|
|
||||||
if let Some(entry) = entry {
|
|
||||||
let mut dst_path: PathBuf = PathBuf::from(input);
|
|
||||||
// Check if path is relative
|
|
||||||
if dst_path.as_path().is_relative() {
|
|
||||||
let mut wrkdir: PathBuf = self.local.wrkdir.clone();
|
|
||||||
wrkdir.push(dst_path);
|
|
||||||
dst_path = wrkdir;
|
|
||||||
}
|
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
|
||||||
// Rename file or directory and report status as popup
|
|
||||||
match self
|
|
||||||
.context
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.local
|
|
||||||
.rename(&entry, dst_path.as_path())
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
// Reload files
|
|
||||||
let path: PathBuf = self.local.wrkdir.clone();
|
|
||||||
self.local_scan(path.as_path());
|
|
||||||
// Log
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!(
|
|
||||||
"Renamed file \"{}\" to \"{}\"",
|
|
||||||
full_path.display(),
|
|
||||||
dst_path.display()
|
|
||||||
)
|
|
||||||
.as_ref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not rename file \"{}\": {}", full_path.display(), err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_remote_rename(&mut self, input: String) {
|
|
||||||
if let Some(idx) = self.get_remote_file_idx() {
|
|
||||||
if let Some(entry) = self.remote.get(idx) {
|
|
||||||
let dst_path: PathBuf = PathBuf::from(input);
|
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
|
||||||
// Rename file or directory and report status as popup
|
|
||||||
match self.client.as_mut().rename(entry, dst_path.as_path()) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Reload files
|
|
||||||
let path: PathBuf = self.remote.wrkdir.clone();
|
|
||||||
self.remote_scan(path.as_path());
|
|
||||||
// Log
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!(
|
|
||||||
"Renamed file \"{}\" to \"{}\"",
|
|
||||||
full_path.display(),
|
|
||||||
dst_path.display()
|
|
||||||
)
|
|
||||||
.as_ref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not rename file \"{}\": {}", full_path.display(), err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_local_delete(&mut self) {
|
|
||||||
let entry: Option<FsEntry> = self.get_local_file_entry().cloned();
|
|
||||||
if let Some(entry) = entry {
|
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
|
||||||
// Delete file or directory and report status as popup
|
|
||||||
match self.context.as_mut().unwrap().local.remove(&entry) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Reload files
|
|
||||||
let p: PathBuf = self.local.wrkdir.clone();
|
|
||||||
self.local_scan(p.as_path());
|
|
||||||
// Log
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Removed file \"{}\"", full_path.display()).as_ref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not delete file \"{}\": {}", full_path.display(), err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_remote_delete(&mut self) {
|
|
||||||
if let Some(idx) = self.get_remote_file_idx() {
|
|
||||||
// Check if file entry exists
|
|
||||||
if let Some(entry) = self.remote.get(idx) {
|
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
|
||||||
// Delete file
|
|
||||||
match self.client.remove(entry) {
|
|
||||||
Ok(_) => {
|
|
||||||
self.reload_remote_dir();
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Removed file \"{}\"", full_path.display()).as_ref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not delete file \"{}\": {}", full_path.display(), err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_local_saveas(&mut self, input: String) {
|
|
||||||
if let Some(idx) = self.get_local_file_idx() {
|
|
||||||
// Get pwd
|
|
||||||
let wrkdir: PathBuf = self.remote.wrkdir.clone();
|
|
||||||
if self.local.get(idx).is_some() {
|
|
||||||
let file: FsEntry = self.local.get(idx).unwrap().clone();
|
|
||||||
// Call upload; pass realfile, keep link name
|
|
||||||
self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(input));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_remote_saveas(&mut self, input: String) {
|
|
||||||
if let Some(idx) = self.get_remote_file_idx() {
|
|
||||||
// Get pwd
|
|
||||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
|
||||||
if self.remote.get(idx).is_some() {
|
|
||||||
let file: FsEntry = self.remote.get(idx).unwrap().clone();
|
|
||||||
// Call upload; pass realfile, keep link name
|
|
||||||
self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(input));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_local_newfile(&mut self, input: String) {
|
|
||||||
// Check if file exists
|
|
||||||
let mut file_exists: bool = false;
|
|
||||||
for file in self.local.iter_files_all() {
|
|
||||||
if input == file.get_name() {
|
|
||||||
file_exists = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if file_exists {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Warn,
|
|
||||||
format!("File \"{}\" already exists", input,),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Create file
|
|
||||||
let file_path: PathBuf = PathBuf::from(input.as_str());
|
|
||||||
if let Some(ctx) = self.context.as_mut() {
|
|
||||||
if let Err(err) = ctx.local.open_file_write(file_path.as_path()) {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not create file \"{}\": {}", file_path.display(), err),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Created file \"{}\"", file_path.display()).as_str(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Reload files
|
|
||||||
let path: PathBuf = self.local.wrkdir.clone();
|
|
||||||
self.local_scan(path.as_path());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_remote_newfile(&mut self, input: String) {
|
|
||||||
// Check if file exists
|
|
||||||
let mut file_exists: bool = false;
|
|
||||||
for file in self.remote.iter_files_all() {
|
|
||||||
if input == file.get_name() {
|
|
||||||
file_exists = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if file_exists {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Warn,
|
|
||||||
format!("File \"{}\" already exists", input,),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Get path on remote
|
|
||||||
let file_path: PathBuf = PathBuf::from(input.as_str());
|
|
||||||
// Create file (on local)
|
|
||||||
match tempfile::NamedTempFile::new() {
|
|
||||||
Err(err) => self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not create tempfile: {}", err),
|
|
||||||
),
|
|
||||||
Ok(tfile) => {
|
|
||||||
// Stat tempfile
|
|
||||||
if let Some(ctx) = self.context.as_mut() {
|
|
||||||
let local_file: FsEntry = match ctx.local.stat(tfile.path()) {
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not stat tempfile: {}", err),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Ok(f) => f,
|
|
||||||
};
|
|
||||||
if let FsEntry::File(local_file) = local_file {
|
|
||||||
// Create file
|
|
||||||
match self.client.send_file(&local_file, file_path.as_path()) {
|
|
||||||
Err(err) => self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not create file \"{}\": {}",
|
|
||||||
file_path.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Ok(writer) => {
|
|
||||||
// Finalize write
|
|
||||||
if let Err(err) = self.client.on_sent(writer) {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Warn,
|
|
||||||
format!("Could not finalize file: {}", err),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Created file \"{}\"", file_path.display())
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Reload files
|
|
||||||
let path: PathBuf = self.remote.wrkdir.clone();
|
|
||||||
self.remote_scan(path.as_path());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_local_exec(&mut self, input: String) {
|
|
||||||
match self.context.as_mut().unwrap().local.exec(input.as_str()) {
|
|
||||||
Ok(output) => {
|
|
||||||
// Reload files
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("\"{}\": {}", input, output).as_ref(),
|
|
||||||
);
|
|
||||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
|
||||||
self.local_scan(wrkdir.as_path());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
// Report err
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not execute command \"{}\": {}", input, err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_remote_exec(&mut self, input: String) {
|
|
||||||
match self.client.as_mut().exec(input.as_str()) {
|
|
||||||
Ok(output) => {
|
|
||||||
// Reload files
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("\"{}\": {}", input, output).as_ref(),
|
|
||||||
);
|
|
||||||
self.reload_remote_dir();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
// Report err
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not execute command \"{}\": {}", input, err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_local_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> {
|
|
||||||
match self.context.as_mut().unwrap().local.find(input.as_str()) {
|
|
||||||
Ok(entries) => Ok(entries),
|
|
||||||
Err(err) => Err(format!("Could not search for files: {}", err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_remote_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> {
|
|
||||||
match self.client.as_mut().find(input.as_str()) {
|
|
||||||
Ok(entries) => Ok(entries),
|
|
||||||
Err(err) => Err(format!("Could not search for files: {}", err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_find_changedir(&mut self, idx: usize) {
|
|
||||||
// Match entry
|
|
||||||
if let Some(entry) = self.found.as_ref().unwrap().get(idx) {
|
|
||||||
// Get path: if a directory, use directory path; if it is a File, get parent path
|
|
||||||
let path: PathBuf = match entry {
|
|
||||||
FsEntry::Directory(dir) => dir.abs_path.clone(),
|
|
||||||
FsEntry::File(file) => match file.abs_path.parent() {
|
|
||||||
None => PathBuf::from("."),
|
|
||||||
Some(p) => p.to_path_buf(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// Change directory
|
|
||||||
match self.tab {
|
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
|
||||||
self.local_changedir(path.as_path(), true)
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
self.remote_changedir(path.as_path(), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_find_transfer(&mut self, idx: usize, name: Option<String>) {
|
|
||||||
let entry: Option<FsEntry> = self.found.as_ref().unwrap().get(idx).cloned();
|
|
||||||
if let Some(entry) = entry {
|
|
||||||
// Download file
|
|
||||||
match self.tab {
|
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
|
||||||
let wrkdir: PathBuf = self.remote.wrkdir.clone();
|
|
||||||
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), name);
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
|
||||||
self.filetransfer_recv(&entry.get_realfile(), wrkdir.as_path(), name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn action_find_delete(&mut self, idx: usize) {
|
|
||||||
let entry: Option<FsEntry> = self.found.as_ref().unwrap().get(idx).cloned();
|
|
||||||
if let Some(entry) = entry {
|
|
||||||
// Download file
|
|
||||||
match self.tab {
|
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
|
||||||
// Delete file or directory and report status as popup
|
|
||||||
match self.context.as_mut().unwrap().local.remove(&entry) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Reload files
|
|
||||||
let p: PathBuf = self.local.wrkdir.clone();
|
|
||||||
self.local_scan(p.as_path());
|
|
||||||
// Log
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Removed file \"{}\"", full_path.display()).as_ref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not delete file \"{}\": {}",
|
|
||||||
full_path.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
|
||||||
// Delete file
|
|
||||||
match self.client.remove(&entry) {
|
|
||||||
Ok(_) => {
|
|
||||||
self.reload_remote_dir();
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Removed file \"{}\"", full_path.display()).as_ref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not delete file \"{}\": {}",
|
|
||||||
full_path.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_local_file_entry
|
|
||||||
///
|
|
||||||
/// Get local file entry
|
|
||||||
pub(super) fn get_local_file_entry(&self) -> Option<&FsEntry> {
|
|
||||||
match self.get_local_file_idx() {
|
|
||||||
None => None,
|
|
||||||
Some(idx) => self.local.get(idx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_remote_file_entry
|
|
||||||
///
|
|
||||||
/// Get remote file entry
|
|
||||||
pub(super) fn get_remote_file_entry(&self) -> Option<&FsEntry> {
|
|
||||||
match self.get_remote_file_idx() {
|
|
||||||
None => None,
|
|
||||||
Some(idx) => self.remote.get(idx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- private
|
|
||||||
|
|
||||||
/// ### get_local_file_idx
|
|
||||||
///
|
|
||||||
/// Get index of selected file in the local tab
|
|
||||||
fn get_local_file_idx(&self) -> Option<usize> {
|
|
||||||
match self.view.get_value(super::COMPONENT_EXPLORER_LOCAL) {
|
|
||||||
Some(Payload::Unsigned(idx)) => Some(idx),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_remote_file_idx
|
|
||||||
///
|
|
||||||
/// Get index of selected file in the remote file
|
|
||||||
fn get_remote_file_idx(&self) -> Option<usize> {
|
|
||||||
match self.view.get_value(super::COMPONENT_EXPLORER_REMOTE) {
|
|
||||||
Some(Payload::Unsigned(idx)) => Some(idx),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,12 +28,10 @@
|
|||||||
*/
|
*/
|
||||||
// Locals
|
// Locals
|
||||||
use super::context::Context;
|
use super::context::Context;
|
||||||
// keymap
|
|
||||||
pub(crate) mod keymap;
|
|
||||||
// Activities
|
// Activities
|
||||||
pub mod auth_activity;
|
pub mod auth;
|
||||||
pub mod filetransfer_activity;
|
pub mod filetransfer;
|
||||||
pub mod setup_activity;
|
pub mod setup;
|
||||||
|
|
||||||
// -- Exit reason
|
// -- Exit reason
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,10 @@
|
|||||||
*/
|
*/
|
||||||
// Locals
|
// Locals
|
||||||
use super::SetupActivity;
|
use super::SetupActivity;
|
||||||
use crate::ui::layout::Payload;
|
|
||||||
// Ext
|
// Ext
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use tuirealm::{Payload, Value};
|
||||||
|
|
||||||
impl SetupActivity {
|
impl SetupActivity {
|
||||||
/// ### action_save_config
|
/// ### action_save_config
|
||||||
@@ -63,8 +63,8 @@ impl SetupActivity {
|
|||||||
// Get key
|
// Get key
|
||||||
if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||||
// get index
|
// get index
|
||||||
let idx: Option<usize> = match self.view.get_value(super::COMPONENT_LIST_SSH_KEYS) {
|
let idx: Option<usize> = match self.view.get_state(super::COMPONENT_LIST_SSH_KEYS) {
|
||||||
Some(Payload::Unsigned(idx)) => Some(idx),
|
Some(Payload::One(Value::Usize(idx))) => Some(idx),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(idx) = idx {
|
if let Some(idx) = idx {
|
||||||
@@ -99,25 +99,29 @@ impl SetupActivity {
|
|||||||
pub(super) fn action_new_ssh_key(&mut self) {
|
pub(super) fn action_new_ssh_key(&mut self) {
|
||||||
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||||
// get parameters
|
// get parameters
|
||||||
let host: String = match self.view.get_value(super::COMPONENT_INPUT_SSH_HOST) {
|
let host: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_HOST) {
|
||||||
Some(Payload::Text(host)) => host,
|
Some(Payload::One(Value::Str(host))) => host,
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
};
|
};
|
||||||
let username: String = match self.view.get_value(super::COMPONENT_INPUT_SSH_USERNAME) {
|
let username: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_USERNAME) {
|
||||||
Some(Payload::Text(user)) => user,
|
Some(Payload::One(Value::Str(user))) => user,
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
};
|
};
|
||||||
// Prepare text editor
|
// Prepare text editor
|
||||||
env::set_var("EDITOR", cli.get_text_editor());
|
env::set_var("EDITOR", cli.get_text_editor());
|
||||||
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
||||||
// Put input mode back to normal
|
// Put input mode back to normal
|
||||||
let _ = disable_raw_mode();
|
if let Err(err) = disable_raw_mode() {
|
||||||
|
error!("Failed to disable raw mode: {}", err);
|
||||||
|
}
|
||||||
// Leave alternate mode
|
// Leave alternate mode
|
||||||
if let Some(ctx) = self.context.as_mut() {
|
if let Some(ctx) = self.context.as_mut() {
|
||||||
ctx.leave_alternate_screen();
|
ctx.leave_alternate_screen();
|
||||||
}
|
}
|
||||||
// Re-enable raw mode
|
// Re-enable raw mode
|
||||||
let _ = enable_raw_mode();
|
if let Err(err) = enable_raw_mode() {
|
||||||
|
error!("Failed to enter raw mode: {}", err);
|
||||||
|
}
|
||||||
// Write key to file
|
// Write key to file
|
||||||
match edit::edit(placeholder.as_bytes()) {
|
match edit::edit(placeholder.as_bytes()) {
|
||||||
Ok(rsa_key) => {
|
Ok(rsa_key) => {
|
||||||
@@ -88,7 +88,9 @@ impl SetupActivity {
|
|||||||
env::set_var("EDITOR", config_cli.get_text_editor());
|
env::set_var("EDITOR", config_cli.get_text_editor());
|
||||||
}
|
}
|
||||||
// Prepare terminal
|
// Prepare terminal
|
||||||
let _ = disable_raw_mode();
|
if let Err(err) = disable_raw_mode() {
|
||||||
|
error!("Failed to disable raw mode: {}", err);
|
||||||
|
}
|
||||||
// Leave alternate mode
|
// Leave alternate mode
|
||||||
ctx.leave_alternate_screen();
|
ctx.leave_alternate_screen();
|
||||||
// Get result
|
// Get result
|
||||||
@@ -121,7 +123,9 @@ impl SetupActivity {
|
|||||||
// Enter alternate mode
|
// Enter alternate mode
|
||||||
ctx.enter_alternate_screen();
|
ctx.enter_alternate_screen();
|
||||||
// Re-enable raw mode
|
// Re-enable raw mode
|
||||||
let _ = enable_raw_mode();
|
if let Err(err) = enable_raw_mode() {
|
||||||
|
error!("Failed to enter raw mode: {}", err);
|
||||||
|
}
|
||||||
// Return result
|
// Return result
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@@ -34,13 +34,13 @@ mod view;
|
|||||||
|
|
||||||
// Deps
|
// Deps
|
||||||
extern crate crossterm;
|
extern crate crossterm;
|
||||||
extern crate tui;
|
extern crate tuirealm;
|
||||||
|
|
||||||
// Locals
|
// Locals
|
||||||
use super::{Activity, Context, ExitReason};
|
use super::{Activity, Context, ExitReason};
|
||||||
use crate::ui::layout::view::View;
|
|
||||||
// Ext
|
// Ext
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||||
|
use tuirealm::View;
|
||||||
|
|
||||||
// -- components
|
// -- components
|
||||||
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
|
||||||
@@ -53,7 +53,8 @@ const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL";
|
|||||||
const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES";
|
const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES";
|
||||||
const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES";
|
const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES";
|
||||||
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
|
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
|
||||||
const COMPONENT_INPUT_FILE_FMT: &str = "INPUT_FILE_FMT";
|
const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT";
|
||||||
|
const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT";
|
||||||
const COMPONENT_RADIO_TAB: &str = "RADIO_TAB";
|
const COMPONENT_RADIO_TAB: &str = "RADIO_TAB";
|
||||||
const COMPONENT_LIST_SSH_KEYS: &str = "LIST_SSH_KEYS";
|
const COMPONENT_LIST_SSH_KEYS: &str = "LIST_SSH_KEYS";
|
||||||
const COMPONENT_INPUT_SSH_HOST: &str = "INPUT_SSH_HOST";
|
const COMPONENT_INPUT_SSH_HOST: &str = "INPUT_SSH_HOST";
|
||||||
@@ -109,7 +110,9 @@ impl Activity for SetupActivity {
|
|||||||
// Clear terminal
|
// Clear terminal
|
||||||
self.context.as_mut().unwrap().clear_screen();
|
self.context.as_mut().unwrap().clear_screen();
|
||||||
// Put raw mode on enabled
|
// Put raw mode on enabled
|
||||||
let _ = enable_raw_mode();
|
if let Err(err) = enable_raw_mode() {
|
||||||
|
error!("Failed to enter raw mode: {}", err);
|
||||||
|
}
|
||||||
// Init view
|
// Init view
|
||||||
self.init_setup();
|
self.init_setup();
|
||||||
// Verify error state from context
|
// Verify error state from context
|
||||||
@@ -160,7 +163,9 @@ impl Activity for SetupActivity {
|
|||||||
/// This function finally releases the context
|
/// This function finally releases the context
|
||||||
fn on_destroy(&mut self) -> Option<Context> {
|
fn on_destroy(&mut self) -> Option<Context> {
|
||||||
// Disable raw mode
|
// Disable raw mode
|
||||||
let _ = disable_raw_mode();
|
if let Err(err) = disable_raw_mode() {
|
||||||
|
error!("Failed to disable raw mode: {}", err);
|
||||||
|
}
|
||||||
self.context.as_ref()?;
|
self.context.as_ref()?;
|
||||||
// Clear terminal and return
|
// Clear terminal and return
|
||||||
match self.context.take() {
|
match self.context.take() {
|
||||||
@@ -28,14 +28,16 @@
|
|||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{
|
use super::{
|
||||||
SetupActivity, COMPONENT_INPUT_FILE_FMT, COMPONENT_INPUT_SSH_HOST,
|
SetupActivity, COMPONENT_INPUT_LOCAL_FILE_FMT, COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||||
COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS,
|
COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR,
|
||||||
COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY, COMPONENT_RADIO_GROUP_DIRS,
|
COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY,
|
||||||
COMPONENT_RADIO_HIDDEN_FILES, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE,
|
COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES, COMPONENT_RADIO_QUIT,
|
||||||
COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP,
|
COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP,
|
||||||
};
|
};
|
||||||
use crate::ui::activities::keymap::*;
|
use crate::ui::keymap::*;
|
||||||
use crate::ui::layout::{Msg, Payload};
|
|
||||||
|
// ext
|
||||||
|
use tuirealm::{Msg, Payload, Value};
|
||||||
|
|
||||||
impl SetupActivity {
|
impl SetupActivity {
|
||||||
/// ### update
|
/// ### update
|
||||||
@@ -66,15 +68,23 @@ impl SetupActivity {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_RADIO_GROUP_DIRS, &MSG_KEY_DOWN) => {
|
(COMPONENT_RADIO_GROUP_DIRS, &MSG_KEY_DOWN) => {
|
||||||
self.view.active(COMPONENT_INPUT_FILE_FMT);
|
self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_FILE_FMT, &MSG_KEY_DOWN) => {
|
(COMPONENT_INPUT_LOCAL_FILE_FMT, &MSG_KEY_DOWN) => {
|
||||||
|
self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
(COMPONENT_INPUT_REMOTE_FILE_FMT, &MSG_KEY_DOWN) => {
|
||||||
self.view.active(COMPONENT_INPUT_TEXT_EDITOR);
|
self.view.active(COMPONENT_INPUT_TEXT_EDITOR);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
// Input field <UP>
|
// Input field <UP>
|
||||||
(COMPONENT_INPUT_FILE_FMT, &MSG_KEY_UP) => {
|
(COMPONENT_INPUT_REMOTE_FILE_FMT, &MSG_KEY_UP) => {
|
||||||
|
self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
(COMPONENT_INPUT_LOCAL_FILE_FMT, &MSG_KEY_UP) => {
|
||||||
self.view.active(COMPONENT_RADIO_GROUP_DIRS);
|
self.view.active(COMPONENT_RADIO_GROUP_DIRS);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -95,7 +105,7 @@ impl SetupActivity {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_TEXT_EDITOR, &MSG_KEY_UP) => {
|
(COMPONENT_INPUT_TEXT_EDITOR, &MSG_KEY_UP) => {
|
||||||
self.view.active(COMPONENT_INPUT_FILE_FMT);
|
self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
// Error <ENTER> or <ESC>
|
// Error <ENTER> or <ESC>
|
||||||
@@ -105,7 +115,7 @@ impl SetupActivity {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
// Exit
|
// Exit
|
||||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(0))) => {
|
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||||
// Save changes
|
// Save changes
|
||||||
if let Err(err) = self.action_save_config() {
|
if let Err(err) = self.action_save_config() {
|
||||||
self.mount_error(err.as_str());
|
self.mount_error(err.as_str());
|
||||||
@@ -114,7 +124,7 @@ impl SetupActivity {
|
|||||||
self.exit_reason = Some(super::ExitReason::Quit);
|
self.exit_reason = Some(super::ExitReason::Quit);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(1))) => {
|
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
|
||||||
// Quit
|
// Quit
|
||||||
self.exit_reason = Some(super::ExitReason::Quit);
|
self.exit_reason = Some(super::ExitReason::Quit);
|
||||||
self.umount_quit();
|
self.umount_quit();
|
||||||
@@ -132,7 +142,7 @@ impl SetupActivity {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
// Delete key
|
// Delete key
|
||||||
(COMPONENT_RADIO_DEL_SSH_KEY, Msg::OnSubmit(Payload::Unsigned(0))) => {
|
(COMPONENT_RADIO_DEL_SSH_KEY, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||||
// Delete key
|
// Delete key
|
||||||
self.action_delete_ssh_key();
|
self.action_delete_ssh_key();
|
||||||
// Reload ssh keys
|
// Reload ssh keys
|
||||||
@@ -147,7 +157,7 @@ impl SetupActivity {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
// Save popup
|
// Save popup
|
||||||
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::Unsigned(0))) => {
|
(COMPONENT_RADIO_SAVE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
|
||||||
// Save config
|
// Save config
|
||||||
if let Err(err) = self.action_save_config() {
|
if let Err(err) = self.action_save_config() {
|
||||||
self.mount_error(err.as_str());
|
self.mount_error(err.as_str());
|
||||||
@@ -216,7 +226,7 @@ impl SetupActivity {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
// <ENTER> Edit key
|
// <ENTER> Edit key
|
||||||
(COMPONENT_LIST_SSH_KEYS, Msg::OnSubmit(Payload::Unsigned(idx))) => {
|
(COMPONENT_LIST_SSH_KEYS, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
|
||||||
// Edit ssh key
|
// Edit ssh key
|
||||||
if let Err(err) = self.edit_ssh_key(*idx) {
|
if let Err(err) = self.edit_ssh_key(*idx) {
|
||||||
self.mount_error(err.as_str());
|
self.mount_error(err.as_str());
|
||||||
@@ -30,22 +30,27 @@
|
|||||||
use super::{Context, SetupActivity, ViewLayout};
|
use super::{Context, SetupActivity, ViewLayout};
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
use crate::fs::explorer::GroupDirs;
|
use crate::fs::explorer::GroupDirs;
|
||||||
use crate::ui::layout::components::{
|
use crate::ui::components::{
|
||||||
bookmark_list::BookmarkList, input::Input, msgbox::MsgBox, radio_group::RadioGroup,
|
bookmark_list::{BookmarkList, BookmarkListPropsBuilder},
|
||||||
table::Table, text::Text,
|
msgbox::{MsgBox, MsgBoxPropsBuilder},
|
||||||
};
|
};
|
||||||
use crate::ui::layout::props::{
|
use crate::utils::ui::draw_area_in;
|
||||||
PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
|
|
||||||
};
|
|
||||||
use crate::ui::layout::utils::draw_area_in;
|
|
||||||
use crate::ui::layout::view::View;
|
|
||||||
use crate::ui::layout::Payload;
|
|
||||||
// Ext
|
// Ext
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tui::{
|
use tuirealm::components::{
|
||||||
|
input::{Input, InputPropsBuilder},
|
||||||
|
radio::{Radio, RadioPropsBuilder},
|
||||||
|
span::{Span, SpanPropsBuilder},
|
||||||
|
table::{Table, TablePropsBuilder},
|
||||||
|
};
|
||||||
|
use tuirealm::tui::{
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
style::Color,
|
style::Color,
|
||||||
widgets::{Borders, Clear},
|
widgets::{BorderType, Borders, Clear},
|
||||||
|
};
|
||||||
|
use tuirealm::{
|
||||||
|
props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
|
||||||
|
Payload, Value, View,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl SetupActivity {
|
impl SetupActivity {
|
||||||
@@ -61,38 +66,32 @@ impl SetupActivity {
|
|||||||
// Radio tab
|
// Radio tab
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_TAB,
|
super::COMPONENT_RADIO_TAB,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::LightYellow)
|
.with_color(Color::LightYellow)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_borders(Borders::BOTTOM)
|
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::White)
|
||||||
.with_texts(TextParts::new(
|
.with_options(
|
||||||
None,
|
None,
|
||||||
Some(vec![
|
vec![String::from("User Interface"), String::from("SSH Keys")],
|
||||||
TextSpan::from("User Interface"),
|
)
|
||||||
TextSpan::from("SSH Keys"),
|
.with_value(0)
|
||||||
]),
|
|
||||||
))
|
|
||||||
.with_value(PropValue::Unsigned(0))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
// Footer
|
// Footer
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_FOOTER,
|
super::COMPONENT_TEXT_FOOTER,
|
||||||
Box::new(Text::new(
|
Box::new(Span::new(
|
||||||
PropsBuilder::default()
|
SpanPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.with_spans(vec![
|
||||||
None,
|
TextSpanBuilder::new("Press ").bold().build(),
|
||||||
Some(vec![
|
TextSpanBuilder::new("<CTRL+H>")
|
||||||
TextSpanBuilder::new("Press ").bold().build(),
|
.bold()
|
||||||
TextSpanBuilder::new("<CTRL+H>")
|
.with_foreground(Color::Cyan)
|
||||||
.bold()
|
.build(),
|
||||||
.with_foreground(Color::Cyan)
|
TextSpanBuilder::new(" to show keybindings").bold().build(),
|
||||||
.build(),
|
])
|
||||||
TextSpanBuilder::new(" to show keybindings").bold().build(),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -100,83 +99,96 @@ impl SetupActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_TEXT_EDITOR,
|
super::COMPONENT_INPUT_TEXT_EDITOR,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_foreground(Color::LightGreen)
|
.with_foreground(Color::LightGreen)
|
||||||
.with_texts(TextParts::new(Some(String::from("Text editor")), None))
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||||
|
.with_label(String::from("Text editor"))
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
|
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::LightCyan)
|
.with_color(Color::LightCyan)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Default file transfer protocol")),
|
Some(String::from("Default file transfer protocol")),
|
||||||
Some(vec![
|
vec![
|
||||||
TextSpan::from("SFTP"),
|
String::from("SFTP"),
|
||||||
TextSpan::from("SCP"),
|
String::from("SCP"),
|
||||||
TextSpan::from("FTP"),
|
String::from("FTP"),
|
||||||
TextSpan::from("FTPS"),
|
String::from("FTPS"),
|
||||||
]),
|
],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_HIDDEN_FILES,
|
super::COMPONENT_RADIO_HIDDEN_FILES,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::LightRed)
|
.with_color(Color::LightRed)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Show hidden files (by default)")),
|
Some(String::from("Show hidden files (by default)")),
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
vec![String::from("Yes"), String::from("No")],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_UPDATES,
|
super::COMPONENT_RADIO_UPDATES,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::LightYellow)
|
.with_color(Color::LightYellow)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Check for updates?")),
|
Some(String::from("Check for updates?")),
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
vec![String::from("Yes"), String::from("No")],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_GROUP_DIRS,
|
super::COMPONENT_RADIO_GROUP_DIRS,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::LightMagenta)
|
.with_color(Color::LightMagenta)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Group directories")),
|
Some(String::from("Group directories")),
|
||||||
Some(vec![
|
vec![
|
||||||
TextSpan::from("Display first"),
|
String::from("Display first"),
|
||||||
TextSpan::from("Display Last"),
|
String::from("Display Last"),
|
||||||
TextSpan::from("No"),
|
String::from("No"),
|
||||||
]),
|
],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_FILE_FMT,
|
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_foreground(Color::LightBlue)
|
.with_foreground(Color::LightBlue)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
|
||||||
Some(String::from("File formatter syntax")),
|
.with_label(String::from("File formatter syntax (local)"))
|
||||||
None,
|
.build(),
|
||||||
))
|
)),
|
||||||
|
);
|
||||||
|
self.view.mount(
|
||||||
|
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||||
|
Box::new(Input::new(
|
||||||
|
InputPropsBuilder::default()
|
||||||
|
.with_foreground(Color::LightGreen)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||||
|
.with_label(String::from("File formatter syntax (remote)"))
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -196,46 +208,41 @@ impl SetupActivity {
|
|||||||
// Radio tab
|
// Radio tab
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_TAB,
|
super::COMPONENT_RADIO_TAB,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::LightYellow)
|
.with_color(Color::LightYellow)
|
||||||
.with_background(Color::Black)
|
.with_inverted_color(Color::Black)
|
||||||
.with_borders(Borders::BOTTOM)
|
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::LightYellow)
|
||||||
.with_texts(TextParts::new(
|
.with_options(
|
||||||
None,
|
None,
|
||||||
Some(vec![
|
vec![String::from("User Interface"), String::from("SSH Keys")],
|
||||||
TextSpan::from("User Interface"),
|
)
|
||||||
TextSpan::from("SSH Keys"),
|
.with_value(1)
|
||||||
]),
|
|
||||||
))
|
|
||||||
.with_value(PropValue::Unsigned(1))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
// Footer
|
// Footer
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_FOOTER,
|
super::COMPONENT_TEXT_FOOTER,
|
||||||
Box::new(Text::new(
|
Box::new(Span::new(
|
||||||
PropsBuilder::default()
|
SpanPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.with_spans(vec![
|
||||||
None,
|
TextSpanBuilder::new("Press ").bold().build(),
|
||||||
Some(vec![
|
TextSpanBuilder::new("<CTRL+H>")
|
||||||
TextSpanBuilder::new("Press ").bold().build(),
|
.bold()
|
||||||
TextSpanBuilder::new("<CTRL+H>")
|
.with_foreground(Color::Cyan)
|
||||||
.bold()
|
.build(),
|
||||||
.with_foreground(Color::Cyan)
|
TextSpanBuilder::new(" to show keybindings").bold().build(),
|
||||||
.build(),
|
])
|
||||||
TextSpanBuilder::new(" to show keybindings").bold().build(),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_LIST_SSH_KEYS,
|
super::COMPONENT_LIST_SSH_KEYS,
|
||||||
Box::new(BookmarkList::new(
|
Box::new(BookmarkList::new(
|
||||||
PropsBuilder::default()
|
BookmarkListPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(Some(String::from("SSH Keys")), Some(vec![])))
|
.with_bookmarks(Some(String::from("SSH Keys")), vec![])
|
||||||
|
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
|
||||||
.with_background(Color::LightGreen)
|
.with_background(Color::LightGreen)
|
||||||
.with_foreground(Color::Black)
|
.with_foreground(Color::Black)
|
||||||
.build(),
|
.build(),
|
||||||
@@ -283,7 +290,8 @@ impl SetupActivity {
|
|||||||
Constraint::Length(3), // Hidden files
|
Constraint::Length(3), // Hidden files
|
||||||
Constraint::Length(3), // Updates tab
|
Constraint::Length(3), // Updates tab
|
||||||
Constraint::Length(3), // Group dirs
|
Constraint::Length(3), // Group dirs
|
||||||
Constraint::Length(3), // Format input
|
Constraint::Length(3), // Local Format input
|
||||||
|
Constraint::Length(3), // Remote Format input
|
||||||
Constraint::Length(1), // Empty ?
|
Constraint::Length(1), // Empty ?
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
@@ -300,7 +308,9 @@ impl SetupActivity {
|
|||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[4]);
|
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[4]);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_INPUT_FILE_FMT, f, ui_cfg_chunks[5]);
|
.render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[5]);
|
||||||
|
self.view
|
||||||
|
.render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[6]);
|
||||||
}
|
}
|
||||||
ViewLayout::SshKeys => {
|
ViewLayout::SshKeys => {
|
||||||
let sshcfg_chunks = Layout::default()
|
let sshcfg_chunks = Layout::default()
|
||||||
@@ -312,40 +322,40 @@ impl SetupActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Popups
|
// Popups
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
let popup = draw_area_in(f.size(), 50, 10);
|
let popup = draw_area_in(f.size(), 50, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
// make popup
|
// make popup
|
||||||
let popup = draw_area_in(f.size(), 40, 10);
|
let popup = draw_area_in(f.size(), 40, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
// make popup
|
// make popup
|
||||||
let popup = draw_area_in(f.size(), 50, 70);
|
let popup = draw_area_in(f.size(), 50, 70);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
// make popup
|
// make popup
|
||||||
let popup = draw_area_in(f.size(), 30, 10);
|
let popup = draw_area_in(f.size(), 30, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
|
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
// make popup
|
// make popup
|
||||||
let popup = draw_area_in(f.size(), 30, 10);
|
let popup = draw_area_in(f.size(), 30, 10);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
@@ -353,8 +363,8 @@ impl SetupActivity {
|
|||||||
.render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup);
|
.render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
|
||||||
if props.build().visible {
|
if props.visible {
|
||||||
// make popup
|
// make popup
|
||||||
let popup = draw_area_in(f.size(), 50, 20);
|
let popup = draw_area_in(f.size(), 50, 20);
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
@@ -389,10 +399,11 @@ impl SetupActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_ERROR,
|
super::COMPONENT_TEXT_ERROR,
|
||||||
Box::new(MsgBox::new(
|
Box::new(MsgBox::new(
|
||||||
PropsBuilder::default()
|
MsgBoxPropsBuilder::default()
|
||||||
.with_foreground(Color::Red)
|
.with_foreground(Color::Red)
|
||||||
.bold()
|
.bold()
|
||||||
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
|
||||||
|
.with_texts(None, vec![TextSpan::from(text)])
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -413,15 +424,16 @@ impl SetupActivity {
|
|||||||
pub(super) fn mount_del_ssh_key(&mut self) {
|
pub(super) fn mount_del_ssh_key(&mut self) {
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_DEL_SSH_KEY,
|
super::COMPONENT_RADIO_DEL_SSH_KEY,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::LightRed)
|
.with_color(Color::LightRed)
|
||||||
.bold()
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Delete key?")),
|
Some(String::from("Delete key?")),
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
vec![String::from("Yes"), String::from("No")],
|
||||||
))
|
)
|
||||||
.with_value(PropValue::Unsigned(1)) // Default: No
|
.with_value(1) // Default: No
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -443,21 +455,26 @@ impl SetupActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_SSH_HOST,
|
super::COMPONENT_INPUT_SSH_HOST,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.with_label(String::from("Hostname or address"))
|
||||||
Some(String::from("Hostname or address")),
|
.with_borders(
|
||||||
None,
|
Borders::TOP | Borders::RIGHT | Borders::LEFT,
|
||||||
))
|
BorderType::Plain,
|
||||||
.with_borders(Borders::TOP | Borders::RIGHT | Borders::LEFT)
|
Color::Reset,
|
||||||
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_INPUT_SSH_USERNAME,
|
super::COMPONENT_INPUT_SSH_USERNAME,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
PropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(Some(String::from("Username")), None))
|
.with_label(String::from("Username"))
|
||||||
.with_borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT)
|
.with_borders(
|
||||||
|
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
|
||||||
|
BorderType::Plain,
|
||||||
|
Color::Reset,
|
||||||
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -478,18 +495,19 @@ impl SetupActivity {
|
|||||||
pub(super) fn mount_quit(&mut self) {
|
pub(super) fn mount_quit(&mut self) {
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_QUIT,
|
super::COMPONENT_RADIO_QUIT,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::LightRed)
|
.with_color(Color::LightRed)
|
||||||
.bold()
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Exit setup?")),
|
Some(String::from("Exit setup?")),
|
||||||
Some(vec![
|
vec![
|
||||||
TextSpan::from("Save"),
|
String::from("Save"),
|
||||||
TextSpan::from("Don't save"),
|
String::from("Don't save"),
|
||||||
TextSpan::from("Cancel"),
|
String::from("Cancel"),
|
||||||
]),
|
],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -510,14 +528,15 @@ impl SetupActivity {
|
|||||||
pub(super) fn mount_save_popup(&mut self) {
|
pub(super) fn mount_save_popup(&mut self) {
|
||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_RADIO_SAVE,
|
super::COMPONENT_RADIO_SAVE,
|
||||||
Box::new(RadioGroup::new(
|
Box::new(Radio::new(
|
||||||
PropsBuilder::default()
|
RadioPropsBuilder::default()
|
||||||
.with_foreground(Color::LightYellow)
|
.with_color(Color::LightYellow)
|
||||||
.bold()
|
.with_inverted_color(Color::Black)
|
||||||
.with_texts(TextParts::new(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||||
|
.with_options(
|
||||||
Some(String::from("Save changes?")),
|
Some(String::from("Save changes?")),
|
||||||
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
|
vec![String::from("Yes"), String::from("No")],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -539,8 +558,9 @@ impl SetupActivity {
|
|||||||
self.view.mount(
|
self.view.mount(
|
||||||
super::COMPONENT_TEXT_HELP,
|
super::COMPONENT_TEXT_HELP,
|
||||||
Box::new(Table::new(
|
Box::new(Table::new(
|
||||||
PropsBuilder::default()
|
TablePropsBuilder::default()
|
||||||
.with_texts(TextParts::table(
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
|
||||||
|
.with_table(
|
||||||
Some(String::from("Help")),
|
Some(String::from("Help")),
|
||||||
TableBuilder::default()
|
TableBuilder::default()
|
||||||
.add_col(
|
.add_col(
|
||||||
@@ -615,7 +635,7 @@ impl SetupActivity {
|
|||||||
)
|
)
|
||||||
.add_col(TextSpan::from(" Save configuration"))
|
.add_col(TextSpan::from(" Save configuration"))
|
||||||
.build(),
|
.build(),
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -636,78 +656,70 @@ impl SetupActivity {
|
|||||||
pub(super) fn load_input_values(&mut self) {
|
pub(super) fn load_input_values(&mut self) {
|
||||||
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||||
// Text editor
|
// Text editor
|
||||||
if let Some(props) = self
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) {
|
||||||
.view
|
|
||||||
.get_props(super::COMPONENT_INPUT_TEXT_EDITOR)
|
|
||||||
.as_mut()
|
|
||||||
{
|
|
||||||
let text_editor: String =
|
let text_editor: String =
|
||||||
String::from(cli.get_text_editor().as_path().to_string_lossy());
|
String::from(cli.get_text_editor().as_path().to_string_lossy());
|
||||||
let props = props.with_value(PropValue::Str(text_editor)).build();
|
let props = InputPropsBuilder::from(props)
|
||||||
|
.with_value(text_editor)
|
||||||
|
.build();
|
||||||
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
|
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
|
||||||
}
|
}
|
||||||
// Protocol
|
// Protocol
|
||||||
if let Some(props) = self
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) {
|
||||||
.view
|
|
||||||
.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
|
|
||||||
.as_mut()
|
|
||||||
{
|
|
||||||
let protocol: usize = match cli.get_default_protocol() {
|
let protocol: usize = match cli.get_default_protocol() {
|
||||||
FileTransferProtocol::Sftp => 0,
|
FileTransferProtocol::Sftp => 0,
|
||||||
FileTransferProtocol::Scp => 1,
|
FileTransferProtocol::Scp => 1,
|
||||||
FileTransferProtocol::Ftp(false) => 2,
|
FileTransferProtocol::Ftp(false) => 2,
|
||||||
FileTransferProtocol::Ftp(true) => 3,
|
FileTransferProtocol::Ftp(true) => 3,
|
||||||
};
|
};
|
||||||
let props = props.with_value(PropValue::Unsigned(protocol)).build();
|
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
|
||||||
let _ = self
|
let _ = self
|
||||||
.view
|
.view
|
||||||
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
|
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
|
||||||
}
|
}
|
||||||
// Hidden files
|
// Hidden files
|
||||||
if let Some(props) = self
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) {
|
||||||
.view
|
|
||||||
.get_props(super::COMPONENT_RADIO_HIDDEN_FILES)
|
|
||||||
.as_mut()
|
|
||||||
{
|
|
||||||
let hidden: usize = match cli.get_show_hidden_files() {
|
let hidden: usize = match cli.get_show_hidden_files() {
|
||||||
true => 0,
|
true => 0,
|
||||||
false => 1,
|
false => 1,
|
||||||
};
|
};
|
||||||
let props = props.with_value(PropValue::Unsigned(hidden)).build();
|
let props = RadioPropsBuilder::from(props).with_value(hidden).build();
|
||||||
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
|
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
|
||||||
}
|
}
|
||||||
// Updates
|
// Updates
|
||||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES).as_mut() {
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) {
|
||||||
let updates: usize = match cli.get_check_for_updates() {
|
let updates: usize = match cli.get_check_for_updates() {
|
||||||
true => 0,
|
true => 0,
|
||||||
false => 1,
|
false => 1,
|
||||||
};
|
};
|
||||||
let props = props.with_value(PropValue::Unsigned(updates)).build();
|
let props = RadioPropsBuilder::from(props).with_value(updates).build();
|
||||||
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
|
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
|
||||||
}
|
}
|
||||||
// Group dirs
|
// Group dirs
|
||||||
if let Some(props) = self
|
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
|
||||||
.view
|
|
||||||
.get_props(super::COMPONENT_RADIO_GROUP_DIRS)
|
|
||||||
.as_mut()
|
|
||||||
{
|
|
||||||
let dirs: usize = match cli.get_group_dirs() {
|
let dirs: usize = match cli.get_group_dirs() {
|
||||||
Some(GroupDirs::First) => 0,
|
Some(GroupDirs::First) => 0,
|
||||||
Some(GroupDirs::Last) => 1,
|
Some(GroupDirs::Last) => 1,
|
||||||
None => 2,
|
None => 2,
|
||||||
};
|
};
|
||||||
let props = props.with_value(PropValue::Unsigned(dirs)).build();
|
let props = RadioPropsBuilder::from(props).with_value(dirs).build();
|
||||||
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
|
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
|
||||||
}
|
}
|
||||||
// File Fmt
|
// Local File Fmt
|
||||||
if let Some(props) = self
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_LOCAL_FILE_FMT) {
|
||||||
.view
|
let file_fmt: String = cli.get_local_file_fmt().unwrap_or_default();
|
||||||
.get_props(super::COMPONENT_INPUT_FILE_FMT)
|
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
|
||||||
.as_mut()
|
let _ = self
|
||||||
{
|
.view
|
||||||
let file_fmt: String = cli.get_file_fmt().unwrap_or_default();
|
.update(super::COMPONENT_INPUT_LOCAL_FILE_FMT, props);
|
||||||
let props = props.with_value(PropValue::Str(file_fmt)).build();
|
}
|
||||||
let _ = self.view.update(super::COMPONENT_INPUT_FILE_FMT, props);
|
// Remote File Fmt
|
||||||
|
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_REMOTE_FILE_FMT) {
|
||||||
|
let file_fmt: String = cli.get_remote_file_fmt().unwrap_or_default();
|
||||||
|
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
|
||||||
|
let _ = self
|
||||||
|
.view
|
||||||
|
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -717,13 +729,13 @@ impl SetupActivity {
|
|||||||
/// Collect values from input and put them into the configuration
|
/// Collect values from input and put them into the configuration
|
||||||
pub(super) fn collect_input_values(&mut self) {
|
pub(super) fn collect_input_values(&mut self) {
|
||||||
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
|
||||||
if let Some(Payload::Text(editor)) =
|
if let Some(Payload::One(Value::Str(editor))) =
|
||||||
self.view.get_value(super::COMPONENT_INPUT_TEXT_EDITOR)
|
self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR)
|
||||||
{
|
{
|
||||||
cli.set_text_editor(PathBuf::from(editor.as_str()));
|
cli.set_text_editor(PathBuf::from(editor.as_str()));
|
||||||
}
|
}
|
||||||
if let Some(Payload::Unsigned(protocol)) =
|
if let Some(Payload::One(Value::Usize(protocol))) =
|
||||||
self.view.get_value(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
|
self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
|
||||||
{
|
{
|
||||||
let protocol: FileTransferProtocol = match protocol {
|
let protocol: FileTransferProtocol = match protocol {
|
||||||
1 => FileTransferProtocol::Scp,
|
1 => FileTransferProtocol::Scp,
|
||||||
@@ -733,23 +745,30 @@ impl SetupActivity {
|
|||||||
};
|
};
|
||||||
cli.set_default_protocol(protocol);
|
cli.set_default_protocol(protocol);
|
||||||
}
|
}
|
||||||
if let Some(Payload::Unsigned(opt)) =
|
if let Some(Payload::One(Value::Usize(opt))) =
|
||||||
self.view.get_value(super::COMPONENT_RADIO_HIDDEN_FILES)
|
self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES)
|
||||||
{
|
{
|
||||||
let show: bool = matches!(opt, 0);
|
let show: bool = matches!(opt, 0);
|
||||||
cli.set_show_hidden_files(show);
|
cli.set_show_hidden_files(show);
|
||||||
}
|
}
|
||||||
if let Some(Payload::Unsigned(opt)) =
|
if let Some(Payload::One(Value::Usize(opt))) =
|
||||||
self.view.get_value(super::COMPONENT_RADIO_UPDATES)
|
self.view.get_state(super::COMPONENT_RADIO_UPDATES)
|
||||||
{
|
{
|
||||||
let check: bool = matches!(opt, 0);
|
let check: bool = matches!(opt, 0);
|
||||||
cli.set_check_for_updates(check);
|
cli.set_check_for_updates(check);
|
||||||
}
|
}
|
||||||
if let Some(Payload::Text(fmt)) = self.view.get_value(super::COMPONENT_INPUT_FILE_FMT) {
|
if let Some(Payload::One(Value::Str(fmt))) =
|
||||||
cli.set_file_fmt(fmt);
|
self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT)
|
||||||
|
{
|
||||||
|
cli.set_local_file_fmt(fmt);
|
||||||
}
|
}
|
||||||
if let Some(Payload::Unsigned(opt)) =
|
if let Some(Payload::One(Value::Str(fmt))) =
|
||||||
self.view.get_value(super::COMPONENT_RADIO_GROUP_DIRS)
|
self.view.get_state(super::COMPONENT_INPUT_REMOTE_FILE_FMT)
|
||||||
|
{
|
||||||
|
cli.set_remote_file_fmt(fmt);
|
||||||
|
}
|
||||||
|
if let Some(Payload::One(Value::Usize(opt))) =
|
||||||
|
self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS)
|
||||||
{
|
{
|
||||||
let dirs: Option<GroupDirs> = match opt {
|
let dirs: Option<GroupDirs> = match opt {
|
||||||
0 => Some(GroupDirs::First),
|
0 => Some(GroupDirs::First),
|
||||||
@@ -767,17 +786,17 @@ impl SetupActivity {
|
|||||||
pub(super) fn reload_ssh_keys(&mut self) {
|
pub(super) fn reload_ssh_keys(&mut self) {
|
||||||
if let Some(cli) = self.context.as_ref().unwrap().config_client.as_ref() {
|
if let Some(cli) = self.context.as_ref().unwrap().config_client.as_ref() {
|
||||||
// get props
|
// get props
|
||||||
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS).as_mut() {
|
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) {
|
||||||
// Create texts
|
// Create texts
|
||||||
let keys: Vec<TextSpan> = cli
|
let keys: Vec<String> = cli
|
||||||
.iter_ssh_keys()
|
.iter_ssh_keys()
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
let (addr, username, _) = cli.get_ssh_key(x).ok().unwrap().unwrap();
|
let (addr, username, _) = cli.get_ssh_key(x).ok().unwrap().unwrap();
|
||||||
TextSpan::from(format!("{} at {}", addr, username).as_str())
|
format!("{} at {}", addr, username)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let props = props
|
let props = BookmarkListPropsBuilder::from(props)
|
||||||
.with_texts(TextParts::new(Some(String::from("SSH Keys")), Some(keys)))
|
.with_bookmarks(Some(String::from("SSH Keys")), keys)
|
||||||
.build();
|
.build();
|
||||||
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
|
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
|
||||||
}
|
}
|
||||||
@@ -25,16 +25,106 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
|
||||||
// ext
|
// ext
|
||||||
use crossterm::event::KeyCode;
|
use tuirealm::components::utils::get_block;
|
||||||
use tui::{
|
use tuirealm::event::{Event, KeyCode};
|
||||||
|
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
|
||||||
|
use tuirealm::tui::{
|
||||||
layout::{Corner, Rect},
|
layout::{Corner, Rect},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
text::Span,
|
text::Span,
|
||||||
widgets::{Block, List, ListItem, ListState},
|
widgets::{BorderType, Borders, List, ListItem, ListState},
|
||||||
};
|
};
|
||||||
|
use tuirealm::{Canvas, Component, Msg, Payload, Value};
|
||||||
|
|
||||||
|
// -- props
|
||||||
|
|
||||||
|
pub struct BookmarkListPropsBuilder {
|
||||||
|
props: Option<Props>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BookmarkListPropsBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
BookmarkListPropsBuilder {
|
||||||
|
props: Some(Props::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropsBuilder for BookmarkListPropsBuilder {
|
||||||
|
fn build(&mut self) -> Props {
|
||||||
|
self.props.take().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hidden(&mut self) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.visible = false;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible(&mut self) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.visible = true;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Props> for BookmarkListPropsBuilder {
|
||||||
|
fn from(props: Props) -> Self {
|
||||||
|
BookmarkListPropsBuilder { props: Some(props) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BookmarkListPropsBuilder {
|
||||||
|
/// ### with_foreground
|
||||||
|
///
|
||||||
|
/// Set foreground color for area
|
||||||
|
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.foreground = color;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### with_background
|
||||||
|
///
|
||||||
|
/// Set background color for area
|
||||||
|
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.background = color;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### with_borders
|
||||||
|
///
|
||||||
|
/// Set component borders style
|
||||||
|
pub fn with_borders(
|
||||||
|
&mut self,
|
||||||
|
borders: Borders,
|
||||||
|
variant: BorderType,
|
||||||
|
color: Color,
|
||||||
|
) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.borders = BordersProps {
|
||||||
|
borders,
|
||||||
|
variant,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_bookmarks(&mut self, title: Option<String>, bookmarks: Vec<String>) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
let bookmarks: Vec<TextSpan> = bookmarks.into_iter().map(TextSpan::from).collect();
|
||||||
|
props.texts = TextParts::new(title, Some(bookmarks));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -- states
|
// -- states
|
||||||
|
|
||||||
@@ -120,7 +210,7 @@ impl BookmarkList {
|
|||||||
// Initialize states
|
// Initialize states
|
||||||
let mut states: OwnStates = OwnStates::default();
|
let mut states: OwnStates = OwnStates::default();
|
||||||
// Set list length
|
// Set list length
|
||||||
states.set_list_len(match &props.texts.rows {
|
states.set_list_len(match &props.texts.spans {
|
||||||
Some(tokens) => tokens.len(),
|
Some(tokens) => tokens.len(),
|
||||||
None => 0,
|
None => 0,
|
||||||
});
|
});
|
||||||
@@ -129,15 +219,11 @@ impl BookmarkList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Component for BookmarkList {
|
impl Component for BookmarkList {
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
|
||||||
/// If focused, cursor is also set (if supported by widget)
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
#[cfg(not(tarpaulin_include))]
|
||||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||||
if self.props.visible {
|
if self.props.visible {
|
||||||
// Make list
|
// Make list
|
||||||
let list_item: Vec<ListItem> = match self.props.texts.rows.as_ref() {
|
let list_item: Vec<ListItem> = match self.props.texts.spans.as_ref() {
|
||||||
None => vec![],
|
None => vec![],
|
||||||
Some(lines) => lines
|
Some(lines) => lines
|
||||||
.iter()
|
.iter()
|
||||||
@@ -148,30 +234,22 @@ impl Component for BookmarkList {
|
|||||||
true => (self.props.foreground, self.props.background),
|
true => (self.props.foreground, self.props.background),
|
||||||
false => (Color::Reset, Color::Reset),
|
false => (Color::Reset, Color::Reset),
|
||||||
};
|
};
|
||||||
let title: String = match self.props.texts.title.as_ref() {
|
|
||||||
Some(t) => t.clone(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
// Render
|
// Render
|
||||||
let mut state: ListState = ListState::default();
|
let mut state: ListState = ListState::default();
|
||||||
state.select(Some(self.states.list_index));
|
state.select(Some(self.states.list_index));
|
||||||
render.render_stateful_widget(
|
render.render_stateful_widget(
|
||||||
List::new(list_item)
|
List::new(list_item)
|
||||||
.block(
|
.block(get_block(
|
||||||
Block::default()
|
&self.props.borders,
|
||||||
.borders(self.props.borders)
|
&self.props.texts.title,
|
||||||
.border_style(match self.states.focus {
|
self.states.focus,
|
||||||
true => Style::default().fg(self.props.background),
|
))
|
||||||
false => Style::default(),
|
|
||||||
})
|
|
||||||
.title(title),
|
|
||||||
)
|
|
||||||
.start_corner(Corner::TopLeft)
|
.start_corner(Corner::TopLeft)
|
||||||
.highlight_style(
|
.highlight_style(
|
||||||
Style::default()
|
Style::default()
|
||||||
.bg(bg)
|
.bg(bg)
|
||||||
.fg(fg)
|
.fg(fg)
|
||||||
.add_modifier(self.props.get_modifiers()),
|
.add_modifier(self.props.modifiers),
|
||||||
),
|
),
|
||||||
area,
|
area,
|
||||||
&mut state,
|
&mut state,
|
||||||
@@ -179,15 +257,10 @@ impl Component for BookmarkList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
fn update(&mut self, props: Props) -> Msg {
|
||||||
self.props = props;
|
self.props = props;
|
||||||
// re-Set list length
|
// re-Set list length
|
||||||
self.states.set_list_len(match &self.props.texts.rows {
|
self.states.set_list_len(match &self.props.texts.spans {
|
||||||
Some(tokens) => tokens.len(),
|
Some(tokens) => tokens.len(),
|
||||||
None => 0,
|
None => 0,
|
||||||
});
|
});
|
||||||
@@ -196,21 +269,13 @@ impl Component for BookmarkList {
|
|||||||
Msg::None
|
Msg::None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get_props
|
fn get_props(&self) -> Props {
|
||||||
///
|
self.props.clone()
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder {
|
|
||||||
PropsBuilder::from(self.props.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### on
|
fn on(&mut self, ev: Event) -> Msg {
|
||||||
///
|
|
||||||
/// Handle input event and update internal states
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
|
||||||
// Match event
|
// Match event
|
||||||
if let InputEvent::Key(key) = ev {
|
if let Event::Key(key) = ev {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
// Update states
|
// Update states
|
||||||
@@ -238,7 +303,7 @@ impl Component for BookmarkList {
|
|||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
// Report event
|
// Report event
|
||||||
Msg::OnSubmit(self.get_value())
|
Msg::OnSubmit(self.get_state())
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Return key event to activity
|
// Return key event to activity
|
||||||
@@ -251,25 +316,14 @@ impl Component for BookmarkList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get_value
|
fn get_state(&self) -> Payload {
|
||||||
///
|
Payload::One(Value::Usize(self.states.get_list_index()))
|
||||||
/// Return component value. File list return index
|
|
||||||
fn get_value(&self) -> Payload {
|
|
||||||
Payload::Unsigned(self.states.get_list_index())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component; basically remove focus
|
|
||||||
fn blur(&mut self) {
|
fn blur(&mut self) {
|
||||||
self.states.focus = false;
|
self.states.focus = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component; basically give focus
|
|
||||||
fn active(&mut self) {
|
fn active(&mut self) {
|
||||||
self.states.focus = true;
|
self.states.focus = true;
|
||||||
}
|
}
|
||||||
@@ -279,21 +333,34 @@ impl Component for BookmarkList {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::ui::layout::props::{TextParts, TextSpan};
|
|
||||||
|
|
||||||
use crossterm::event::KeyEvent;
|
use pretty_assertions::assert_eq;
|
||||||
|
use tuirealm::event::KeyEvent;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ui_layout_components_bookmarks_list() {
|
fn test_ui_components_bookmarks_list() {
|
||||||
// Make component
|
// Make component
|
||||||
let mut component: BookmarkList = BookmarkList::new(
|
let mut component: BookmarkList = BookmarkList::new(
|
||||||
PropsBuilder::default()
|
BookmarkListPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.hidden()
|
||||||
|
.visible()
|
||||||
|
.with_foreground(Color::Red)
|
||||||
|
.with_background(Color::Blue)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||||
|
.with_bookmarks(
|
||||||
Some(String::from("filelist")),
|
Some(String::from("filelist")),
|
||||||
Some(vec![TextSpan::from("file1"), TextSpan::from("file2")]),
|
vec![String::from("file1"), String::from("file2")],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
assert_eq!(component.props.foreground, Color::Red);
|
||||||
|
assert_eq!(component.props.background, Color::Blue);
|
||||||
|
assert_eq!(component.props.visible, true);
|
||||||
|
assert_eq!(
|
||||||
|
component.props.texts.title.as_ref().unwrap().as_str(),
|
||||||
|
"filelist"
|
||||||
|
);
|
||||||
|
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2);
|
||||||
// Verify states
|
// Verify states
|
||||||
assert_eq!(component.states.list_index, 0);
|
assert_eq!(component.states.list_index, 0);
|
||||||
assert_eq!(component.states.list_len, 2);
|
assert_eq!(component.states.list_len, 2);
|
||||||
@@ -304,69 +371,72 @@ mod tests {
|
|||||||
component.blur();
|
component.blur();
|
||||||
assert_eq!(component.states.focus, false);
|
assert_eq!(component.states.focus, false);
|
||||||
// Update
|
// Update
|
||||||
let props = component.get_props().with_foreground(Color::Red).build();
|
let props = BookmarkListPropsBuilder::from(component.get_props())
|
||||||
|
.with_foreground(Color::Yellow)
|
||||||
|
.hidden()
|
||||||
|
.build();
|
||||||
assert_eq!(component.update(props), Msg::None);
|
assert_eq!(component.update(props), Msg::None);
|
||||||
assert_eq!(component.props.foreground, Color::Red);
|
assert_eq!(component.props.foreground, Color::Yellow);
|
||||||
|
assert_eq!(component.props.visible, false);
|
||||||
// Increment list index
|
// Increment list index
|
||||||
component.states.list_index += 1;
|
component.states.list_index += 1;
|
||||||
assert_eq!(component.states.list_index, 1);
|
assert_eq!(component.states.list_index, 1);
|
||||||
// Update
|
// Update
|
||||||
component.update(
|
component.update(
|
||||||
component
|
BookmarkListPropsBuilder::from(component.get_props())
|
||||||
.get_props()
|
.with_bookmarks(
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(String::from("filelist")),
|
Some(String::from("filelist")),
|
||||||
Some(vec![
|
vec![
|
||||||
TextSpan::from("file1"),
|
String::from("file1"),
|
||||||
TextSpan::from("file2"),
|
String::from("file2"),
|
||||||
TextSpan::from("file3"),
|
String::from("file3"),
|
||||||
]),
|
],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
// Verify states
|
// Verify states
|
||||||
assert_eq!(component.states.list_index, 0);
|
assert_eq!(component.states.list_index, 0);
|
||||||
assert_eq!(component.states.list_len, 3);
|
assert_eq!(component.states.list_len, 3);
|
||||||
// get value
|
// get value
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(0));
|
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
|
||||||
// Render
|
// Render
|
||||||
assert_eq!(component.states.list_index, 0);
|
assert_eq!(component.states.list_index, 0);
|
||||||
// Handle inputs
|
// Handle inputs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Down))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||||
Msg::None
|
Msg::None
|
||||||
);
|
);
|
||||||
// Index should be incremented
|
// Index should be incremented
|
||||||
assert_eq!(component.states.list_index, 1);
|
assert_eq!(component.states.list_index, 1);
|
||||||
// Index should be decremented
|
// Index should be decremented
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Up))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
|
||||||
Msg::None
|
Msg::None
|
||||||
);
|
);
|
||||||
// Index should be incremented
|
// Index should be incremented
|
||||||
assert_eq!(component.states.list_index, 0);
|
assert_eq!(component.states.list_index, 0);
|
||||||
// Index should be 2
|
// Index should be 2
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageDown))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||||
Msg::None
|
Msg::None
|
||||||
);
|
);
|
||||||
// Index should be incremented
|
// Index should be incremented
|
||||||
assert_eq!(component.states.list_index, 2);
|
assert_eq!(component.states.list_index, 2);
|
||||||
// Index should be 0
|
// Index should be 0
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageUp))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||||
Msg::None
|
Msg::None
|
||||||
);
|
);
|
||||||
// Index should be incremented
|
// Index should be incremented
|
||||||
assert_eq!(component.states.list_index, 0);
|
assert_eq!(component.states.list_index, 0);
|
||||||
// Enter
|
// Enter
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::Enter))),
|
||||||
Msg::OnSubmit(Payload::Unsigned(0))
|
Msg::OnSubmit(Payload::One(Value::Usize(0)))
|
||||||
);
|
);
|
||||||
// On key
|
// On key
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||||
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,710 @@
|
|||||||
|
//! ## FileList
|
||||||
|
//!
|
||||||
|
//! `FileList` component renders a file list tab
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
// ext
|
||||||
|
use tuirealm::components::utils::get_block;
|
||||||
|
use tuirealm::event::{Event, KeyCode, KeyModifiers};
|
||||||
|
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
|
||||||
|
use tuirealm::tui::{
|
||||||
|
layout::{Corner, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
text::Span,
|
||||||
|
widgets::{BorderType, Borders, List, ListItem, ListState},
|
||||||
|
};
|
||||||
|
use tuirealm::{Canvas, Component, Msg, Payload, Value};
|
||||||
|
|
||||||
|
// -- props
|
||||||
|
|
||||||
|
pub struct FileListPropsBuilder {
|
||||||
|
props: Option<Props>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FileListPropsBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
FileListPropsBuilder {
|
||||||
|
props: Some(Props::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropsBuilder for FileListPropsBuilder {
|
||||||
|
fn build(&mut self) -> Props {
|
||||||
|
self.props.take().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hidden(&mut self) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.visible = false;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible(&mut self) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.visible = true;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Props> for FileListPropsBuilder {
|
||||||
|
fn from(props: Props) -> Self {
|
||||||
|
FileListPropsBuilder { props: Some(props) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileListPropsBuilder {
|
||||||
|
/// ### with_foreground
|
||||||
|
///
|
||||||
|
/// Set foreground color for area
|
||||||
|
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.foreground = color;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### with_background
|
||||||
|
///
|
||||||
|
/// Set background color for area
|
||||||
|
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.background = color;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### with_borders
|
||||||
|
///
|
||||||
|
/// Set component borders style
|
||||||
|
pub fn with_borders(
|
||||||
|
&mut self,
|
||||||
|
borders: Borders,
|
||||||
|
variant: BorderType,
|
||||||
|
color: Color,
|
||||||
|
) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.borders = BordersProps {
|
||||||
|
borders,
|
||||||
|
variant,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_files(&mut self, title: Option<String>, files: Vec<String>) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
let files: Vec<TextSpan> = files.into_iter().map(TextSpan::from).collect();
|
||||||
|
props.texts = TextParts::new(title, Some(files));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- states
|
||||||
|
|
||||||
|
/// ## OwnStates
|
||||||
|
///
|
||||||
|
/// OwnStates contains states for this component
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct OwnStates {
|
||||||
|
list_index: usize, // Index of selected element in list
|
||||||
|
selected: Vec<usize>, // Selected files
|
||||||
|
focus: bool, // Has focus?
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OwnStates {
|
||||||
|
fn default() -> Self {
|
||||||
|
OwnStates {
|
||||||
|
list_index: 0,
|
||||||
|
selected: Vec::new(),
|
||||||
|
focus: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwnStates {
|
||||||
|
/// ### init_list_states
|
||||||
|
///
|
||||||
|
/// Initialize list states
|
||||||
|
pub fn init_list_states(&mut self, len: usize) {
|
||||||
|
self.selected = Vec::with_capacity(len);
|
||||||
|
self.fix_list_index();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### list_index
|
||||||
|
///
|
||||||
|
/// Return current value for list index
|
||||||
|
pub fn list_index(&self) -> usize {
|
||||||
|
self.list_index
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### incr_list_index
|
||||||
|
///
|
||||||
|
/// Incremenet list index
|
||||||
|
pub fn incr_list_index(&mut self) {
|
||||||
|
// Check if index is at last element
|
||||||
|
if self.list_index + 1 < self.list_len() {
|
||||||
|
self.list_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### decr_list_index
|
||||||
|
///
|
||||||
|
/// Decrement list index
|
||||||
|
pub fn decr_list_index(&mut self) {
|
||||||
|
// Check if index is bigger than 0
|
||||||
|
if self.list_index > 0 {
|
||||||
|
self.list_index -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### list_len
|
||||||
|
///
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### is_selected
|
||||||
|
///
|
||||||
|
/// Returns whether the file with index `entry` is selected
|
||||||
|
pub fn is_selected(&self, entry: usize) -> bool {
|
||||||
|
self.selected.contains(&entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### is_selection_empty
|
||||||
|
///
|
||||||
|
/// Returns whether the selection is currently empty
|
||||||
|
pub fn is_selection_empty(&self) -> bool {
|
||||||
|
self.selected.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_selection
|
||||||
|
///
|
||||||
|
/// Returns current file selection
|
||||||
|
pub fn get_selection(&self) -> Vec<usize> {
|
||||||
|
self.selected.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### fix_list_index
|
||||||
|
///
|
||||||
|
/// Keep index if possible, otherwise set to lenght - 1
|
||||||
|
fn fix_list_index(&mut self) {
|
||||||
|
if self.list_index >= self.list_len() && self.list_len() > 0 {
|
||||||
|
self.list_index = self.list_len() - 1;
|
||||||
|
} else if self.list_len() == 0 {
|
||||||
|
self.list_index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- select manipulation
|
||||||
|
|
||||||
|
/// ### toggle_file
|
||||||
|
///
|
||||||
|
/// 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### select_all
|
||||||
|
///
|
||||||
|
/// Select all files
|
||||||
|
pub fn select_all(&mut self) {
|
||||||
|
for i in 0..self.list_len() {
|
||||||
|
self.select(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### select
|
||||||
|
///
|
||||||
|
/// Select provided index if not selected yet
|
||||||
|
fn select(&mut self, entry: usize) {
|
||||||
|
if !self.is_selected(entry) {
|
||||||
|
self.selected.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### deselect
|
||||||
|
///
|
||||||
|
/// Remove element file with associated index
|
||||||
|
fn deselect(&mut self, entry: usize) {
|
||||||
|
if self.is_selected(entry) {
|
||||||
|
self.selected.retain(|&x| x != entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Component
|
||||||
|
|
||||||
|
/// ## FileList
|
||||||
|
///
|
||||||
|
/// File list component
|
||||||
|
pub struct FileList {
|
||||||
|
props: Props,
|
||||||
|
states: OwnStates,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileList {
|
||||||
|
/// ### new
|
||||||
|
///
|
||||||
|
/// Instantiates a new FileList starting from Props
|
||||||
|
/// The method also initializes the component states.
|
||||||
|
pub fn new(props: Props) -> Self {
|
||||||
|
// Initialize states
|
||||||
|
let mut states: OwnStates = OwnStates::default();
|
||||||
|
// Init list states
|
||||||
|
states.init_list_states(props.texts.spans.as_ref().map(|x| x.len()).unwrap_or(0));
|
||||||
|
FileList { props, states }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for FileList {
|
||||||
|
#[cfg(not(tarpaulin_include))]
|
||||||
|
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||||
|
if self.props.visible {
|
||||||
|
// Make list
|
||||||
|
let list_item: Vec<ListItem> = match self.props.texts.spans.as_ref() {
|
||||||
|
None => vec![],
|
||||||
|
Some(lines) => lines
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(num, line)| {
|
||||||
|
let to_display: String = match self.states.is_selected(num) {
|
||||||
|
true => format!("*{}", line.content),
|
||||||
|
false => line.content.to_string(),
|
||||||
|
};
|
||||||
|
ListItem::new(Span::from(to_display))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
let (fg, bg): (Color, Color) = match self.states.focus {
|
||||||
|
true => (Color::Black, self.props.background),
|
||||||
|
false => (self.props.foreground, Color::Reset),
|
||||||
|
};
|
||||||
|
// Render
|
||||||
|
let mut state: ListState = ListState::default();
|
||||||
|
state.select(Some(self.states.list_index));
|
||||||
|
render.render_stateful_widget(
|
||||||
|
List::new(list_item)
|
||||||
|
.block(get_block(
|
||||||
|
&self.props.borders,
|
||||||
|
&self.props.texts.title,
|
||||||
|
self.states.focus,
|
||||||
|
))
|
||||||
|
.start_corner(Corner::TopLeft)
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.bg(bg)
|
||||||
|
.fg(fg)
|
||||||
|
.add_modifier(self.props.modifiers),
|
||||||
|
),
|
||||||
|
area,
|
||||||
|
&mut state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, props: Props) -> Msg {
|
||||||
|
self.props = props;
|
||||||
|
// re-Set list states
|
||||||
|
self.states.init_list_states(
|
||||||
|
self.props
|
||||||
|
.texts
|
||||||
|
.spans
|
||||||
|
.as_ref()
|
||||||
|
.map(|x| x.len())
|
||||||
|
.unwrap_or(0),
|
||||||
|
);
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_props(&self) -> Props {
|
||||||
|
self.props.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on(&mut self, ev: Event) -> Msg {
|
||||||
|
// Match event
|
||||||
|
if let Event::Key(key) = ev {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Down => {
|
||||||
|
// Update states
|
||||||
|
self.states.incr_list_index();
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
// Update states
|
||||||
|
self.states.decr_list_index();
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
KeyCode::PageDown => {
|
||||||
|
// Update states
|
||||||
|
for _ in 0..8 {
|
||||||
|
self.states.incr_list_index();
|
||||||
|
}
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
KeyCode::PageUp => {
|
||||||
|
// Update states
|
||||||
|
for _ in 0..8 {
|
||||||
|
self.states.decr_list_index();
|
||||||
|
}
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
KeyCode::Char('a') => match key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||||
|
// CTRL+A
|
||||||
|
true => {
|
||||||
|
// Select all
|
||||||
|
self.states.select_all();
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
false => Msg::OnKey(key),
|
||||||
|
},
|
||||||
|
KeyCode::Char('m') => {
|
||||||
|
// Toggle current file in selection
|
||||||
|
self.states.toggle_file(self.states.list_index());
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
// Report event
|
||||||
|
Msg::OnSubmit(self.get_state())
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Return key event to activity
|
||||||
|
Msg::OnKey(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unhandled event
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_state
|
||||||
|
///
|
||||||
|
/// Get state returns for this component two different payloads based on the states:
|
||||||
|
/// - if the file selection is empty, returns the highlighted item as `One` of `Usize`
|
||||||
|
/// - if at least one item is selected, return the selected as a `Vec` of `Usize`
|
||||||
|
fn get_state(&self) -> Payload {
|
||||||
|
match self.states.is_selection_empty() {
|
||||||
|
true => Payload::One(Value::Usize(self.states.list_index())),
|
||||||
|
false => Payload::Vec(
|
||||||
|
self.states
|
||||||
|
.get_selection()
|
||||||
|
.into_iter()
|
||||||
|
.map(Value::Usize)
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- events
|
||||||
|
|
||||||
|
/// ### blur
|
||||||
|
///
|
||||||
|
/// Blur component; basically remove focus
|
||||||
|
fn blur(&mut self) {
|
||||||
|
self.states.focus = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### active
|
||||||
|
///
|
||||||
|
/// Active component; basically give focus
|
||||||
|
fn active(&mut self) {
|
||||||
|
self.states.focus = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use tuirealm::event::KeyEvent;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ui_components_file_list_states() {
|
||||||
|
let mut states: OwnStates = OwnStates::default();
|
||||||
|
assert_eq!(states.list_len(), 0);
|
||||||
|
assert_eq!(states.selected.len(), 0);
|
||||||
|
assert_eq!(states.focus, false);
|
||||||
|
// Init states
|
||||||
|
states.init_list_states(4);
|
||||||
|
assert_eq!(states.list_len(), 4);
|
||||||
|
assert_eq!(states.selected.len(), 0);
|
||||||
|
assert!(states.is_selection_empty());
|
||||||
|
// Select all files
|
||||||
|
states.select_all();
|
||||||
|
assert_eq!(states.list_len(), 4);
|
||||||
|
assert_eq!(states.selected.len(), 4);
|
||||||
|
assert_eq!(states.is_selection_empty(), false);
|
||||||
|
assert_eq!(states.get_selection(), vec![0, 1, 2, 3]);
|
||||||
|
// Verify reset
|
||||||
|
states.init_list_states(5);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 0);
|
||||||
|
// Toggle file
|
||||||
|
states.toggle_file(2);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 1);
|
||||||
|
assert_eq!(states.selected[0], 2);
|
||||||
|
states.toggle_file(4);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 2);
|
||||||
|
assert_eq!(states.selected[1], 4);
|
||||||
|
states.toggle_file(2);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 1);
|
||||||
|
assert_eq!(states.selected[0], 4);
|
||||||
|
// Select twice (nothing should change)
|
||||||
|
states.select(4);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 1);
|
||||||
|
assert_eq!(states.selected[0], 4);
|
||||||
|
// Deselect not-selectd item
|
||||||
|
states.deselect(2);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 1);
|
||||||
|
assert_eq!(states.selected[0], 4);
|
||||||
|
// Index
|
||||||
|
states.init_list_states(2);
|
||||||
|
states.incr_list_index();
|
||||||
|
assert_eq!(states.list_index(), 1);
|
||||||
|
states.incr_list_index();
|
||||||
|
assert_eq!(states.list_index(), 1);
|
||||||
|
states.decr_list_index();
|
||||||
|
assert_eq!(states.list_index(), 0);
|
||||||
|
states.decr_list_index();
|
||||||
|
assert_eq!(states.list_index(), 0);
|
||||||
|
// Try fixing index
|
||||||
|
states.init_list_states(5);
|
||||||
|
states.list_index = 4;
|
||||||
|
states.init_list_states(3);
|
||||||
|
assert_eq!(states.list_index(), 2);
|
||||||
|
states.init_list_states(6);
|
||||||
|
assert_eq!(states.list_index(), 2);
|
||||||
|
// Focus
|
||||||
|
states.focus = true;
|
||||||
|
assert_eq!(states.focus, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ui_components_file_list() {
|
||||||
|
// Make component
|
||||||
|
let mut component: FileList = FileList::new(
|
||||||
|
FileListPropsBuilder::default()
|
||||||
|
.hidden()
|
||||||
|
.visible()
|
||||||
|
.with_foreground(Color::Red)
|
||||||
|
.with_background(Color::Blue)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||||
|
.with_files(
|
||||||
|
Some(String::from("files")),
|
||||||
|
vec![String::from("file1"), String::from("file2")],
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
assert_eq!(component.props.foreground, Color::Red);
|
||||||
|
assert_eq!(component.props.background, Color::Blue);
|
||||||
|
assert_eq!(component.props.visible, true);
|
||||||
|
assert_eq!(
|
||||||
|
component.props.texts.title.as_ref().unwrap().as_str(),
|
||||||
|
"files"
|
||||||
|
);
|
||||||
|
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2);
|
||||||
|
// Verify states
|
||||||
|
assert_eq!(component.states.list_index, 0);
|
||||||
|
assert_eq!(component.states.selected.len(), 0);
|
||||||
|
assert_eq!(component.states.list_len(), 2);
|
||||||
|
assert_eq!(component.states.selected.capacity(), 2);
|
||||||
|
assert_eq!(component.states.focus, false);
|
||||||
|
// Focus
|
||||||
|
component.active();
|
||||||
|
assert_eq!(component.states.focus, true);
|
||||||
|
component.blur();
|
||||||
|
assert_eq!(component.states.focus, false);
|
||||||
|
// Update
|
||||||
|
let props = FileListPropsBuilder::from(component.get_props())
|
||||||
|
.with_foreground(Color::Yellow)
|
||||||
|
.hidden()
|
||||||
|
.build();
|
||||||
|
assert_eq!(component.update(props), Msg::None);
|
||||||
|
assert_eq!(component.props.visible, false);
|
||||||
|
assert_eq!(component.props.foreground, Color::Yellow);
|
||||||
|
// Increment list index
|
||||||
|
component.states.list_index += 1;
|
||||||
|
assert_eq!(component.states.list_index, 1);
|
||||||
|
// Update
|
||||||
|
component.update(
|
||||||
|
FileListPropsBuilder::from(component.get_props())
|
||||||
|
.with_files(
|
||||||
|
Some(String::from("filelist")),
|
||||||
|
vec![
|
||||||
|
String::from("file1"),
|
||||||
|
String::from("file2"),
|
||||||
|
String::from("file3"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
// Verify states
|
||||||
|
assert_eq!(component.states.list_index, 1); // Kept
|
||||||
|
assert_eq!(component.states.list_len(), 3);
|
||||||
|
// get value
|
||||||
|
assert_eq!(component.get_state(), Payload::One(Value::Usize(1)));
|
||||||
|
// Render
|
||||||
|
assert_eq!(component.states.list_index, 1);
|
||||||
|
// Handle inputs
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// Index should be incremented
|
||||||
|
assert_eq!(component.states.list_index, 2);
|
||||||
|
// Index should be decremented
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// Index should be incremented
|
||||||
|
assert_eq!(component.states.list_index, 1);
|
||||||
|
// Index should be 2
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// Index should be incremented
|
||||||
|
assert_eq!(component.states.list_index, 2);
|
||||||
|
// Index should be 0
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// Index should be incremented
|
||||||
|
assert_eq!(component.states.list_index, 0);
|
||||||
|
// Enter
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Enter))),
|
||||||
|
Msg::OnSubmit(Payload::One(Value::Usize(0)))
|
||||||
|
);
|
||||||
|
// On key
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||||
|
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
||||||
|
);
|
||||||
|
// Verify 'A' still works
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('a')))),
|
||||||
|
Msg::OnKey(KeyEvent::from(KeyCode::Char('a')))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ui_components_file_list_selection() {
|
||||||
|
// Make component
|
||||||
|
let mut component: FileList = FileList::new(
|
||||||
|
FileListPropsBuilder::default()
|
||||||
|
.with_files(
|
||||||
|
Some(String::from("files")),
|
||||||
|
vec![
|
||||||
|
String::from("file1"),
|
||||||
|
String::from("file2"),
|
||||||
|
String::from("file3"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
// Get state
|
||||||
|
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
|
||||||
|
// Select one
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// Now should be a vec
|
||||||
|
assert_eq!(component.get_state(), Payload::Vec(vec![Value::Usize(0)]));
|
||||||
|
// De-select
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
|
||||||
|
// Go down
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// Select
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
assert_eq!(component.get_state(), Payload::Vec(vec![Value::Usize(1)]));
|
||||||
|
// Go down and select
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
component.get_state(),
|
||||||
|
Payload::Vec(vec![Value::Usize(1), Value::Usize(2)])
|
||||||
|
);
|
||||||
|
// Select all
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('a'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
})),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// All selected
|
||||||
|
assert_eq!(
|
||||||
|
component.get_state(),
|
||||||
|
Payload::Vec(vec![Value::Usize(1), Value::Usize(2), Value::Usize(0)])
|
||||||
|
);
|
||||||
|
// Update files
|
||||||
|
component.update(
|
||||||
|
FileListPropsBuilder::from(component.get_props())
|
||||||
|
.with_files(
|
||||||
|
Some(String::from("filelist")),
|
||||||
|
vec![String::from("file1"), String::from("file2")],
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
// Selection should now be empty
|
||||||
|
assert_eq!(component.get_state(), Payload::One(Value::Usize(1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,17 +25,84 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
|
||||||
// ext
|
// ext
|
||||||
use crossterm::event::KeyCode;
|
use tuirealm::components::utils::{get_block, wrap_spans};
|
||||||
use std::collections::VecDeque;
|
use tuirealm::event::{Event, KeyCode};
|
||||||
use tui::{
|
use tuirealm::props::{BordersProps, Props, PropsBuilder, Table as TextTable, TextParts};
|
||||||
|
use tuirealm::tui::{
|
||||||
layout::{Corner, Rect},
|
layout::{Corner, Rect},
|
||||||
style::Style,
|
style::{Color, Style},
|
||||||
text::{Span, Spans},
|
widgets::{BorderType, Borders, List, ListItem, ListState},
|
||||||
widgets::{Block, List, ListItem, ListState},
|
|
||||||
};
|
};
|
||||||
|
use tuirealm::{Canvas, Component, Msg, Payload, Value};
|
||||||
|
|
||||||
|
// -- props
|
||||||
|
|
||||||
|
pub struct LogboxPropsBuilder {
|
||||||
|
props: Option<Props>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LogboxPropsBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
LogboxPropsBuilder {
|
||||||
|
props: Some(Props::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropsBuilder for LogboxPropsBuilder {
|
||||||
|
fn build(&mut self) -> Props {
|
||||||
|
self.props.take().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hidden(&mut self) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.visible = false;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible(&mut self) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.visible = true;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Props> for LogboxPropsBuilder {
|
||||||
|
fn from(props: Props) -> Self {
|
||||||
|
LogboxPropsBuilder { props: Some(props) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogboxPropsBuilder {
|
||||||
|
/// ### with_borders
|
||||||
|
///
|
||||||
|
/// Set component borders style
|
||||||
|
pub fn with_borders(
|
||||||
|
&mut self,
|
||||||
|
borders: Borders,
|
||||||
|
variant: BorderType,
|
||||||
|
color: Color,
|
||||||
|
) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.borders = BordersProps {
|
||||||
|
borders,
|
||||||
|
variant,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_log(&mut self, title: Option<String>, table: TextTable) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.texts = TextParts::table(title, table);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -- states
|
// -- states
|
||||||
|
|
||||||
@@ -132,82 +199,33 @@ impl LogBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Component for LogBox {
|
impl Component for LogBox {
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
|
||||||
/// If focused, cursor is also set (if supported by widget)
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
#[cfg(not(tarpaulin_include))]
|
||||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||||
if self.props.visible {
|
if self.props.visible {
|
||||||
|
let width: usize = area.width as usize - 4;
|
||||||
// Make list
|
// Make list
|
||||||
let list_items: Vec<ListItem> = match self.props.texts.table.as_ref() {
|
let list_items: Vec<ListItem> = match self.props.texts.table.as_ref() {
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
Some(table) => table
|
Some(table) => table
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| {
|
.map(|row| ListItem::new(wrap_spans(row, width, &self.props)))
|
||||||
let mut columns: VecDeque<Span> = row
|
|
||||||
.iter()
|
|
||||||
.map(|col| {
|
|
||||||
Span::styled(
|
|
||||||
col.content.clone(),
|
|
||||||
Style::default()
|
|
||||||
.add_modifier(col.get_modifiers())
|
|
||||||
.fg(col.fg)
|
|
||||||
.bg(col.bg),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
// Let's convert column spans into Spans rows NOTE: -4 because first line is always made by 5 columns; but there's always 1
|
|
||||||
let mut rows: Vec<Spans> = Vec::with_capacity(columns.len() - 4);
|
|
||||||
// Get first row
|
|
||||||
let mut first_row: Vec<Span> = Vec::with_capacity(5);
|
|
||||||
for _ in 0..5 {
|
|
||||||
if let Some(col) = columns.pop_front() {
|
|
||||||
first_row.push(col);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rows.push(Spans::from(first_row));
|
|
||||||
// Fill remaining rows
|
|
||||||
let cycles: usize = columns.len();
|
|
||||||
for _ in 0..cycles {
|
|
||||||
if let Some(col) = columns.pop_front() {
|
|
||||||
rows.push(Spans::from(vec![col]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListItem::new(rows)
|
|
||||||
})
|
|
||||||
.collect(), // Make List item from TextSpan
|
.collect(), // Make List item from TextSpan
|
||||||
};
|
};
|
||||||
let title: String = match self.props.texts.title.as_ref() {
|
|
||||||
Some(t) => t.clone(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
// Render
|
|
||||||
|
|
||||||
let w = List::new(list_items)
|
let w = List::new(list_items)
|
||||||
.block(
|
.block(get_block(
|
||||||
Block::default()
|
&self.props.borders,
|
||||||
.borders(self.props.borders)
|
&self.props.texts.title,
|
||||||
.border_style(match self.states.focus {
|
self.states.focus,
|
||||||
true => Style::default().fg(self.props.foreground),
|
))
|
||||||
false => Style::default(),
|
|
||||||
})
|
|
||||||
.title(title),
|
|
||||||
)
|
|
||||||
.start_corner(Corner::BottomLeft)
|
.start_corner(Corner::BottomLeft)
|
||||||
.highlight_symbol(">> ")
|
.highlight_symbol(">> ")
|
||||||
.highlight_style(Style::default().add_modifier(self.props.get_modifiers()));
|
.highlight_style(Style::default().add_modifier(self.props.modifiers));
|
||||||
let mut state: ListState = ListState::default();
|
let mut state: ListState = ListState::default();
|
||||||
state.select(Some(self.states.list_index));
|
state.select(Some(self.states.list_index));
|
||||||
render.render_stateful_widget(w, area, &mut state);
|
render.render_stateful_widget(w, area, &mut state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
fn update(&mut self, props: Props) -> Msg {
|
||||||
self.props = props;
|
self.props = props;
|
||||||
// re-Set list length
|
// re-Set list length
|
||||||
@@ -220,21 +238,13 @@ impl Component for LogBox {
|
|||||||
Msg::None
|
Msg::None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get_props
|
fn get_props(&self) -> Props {
|
||||||
///
|
self.props.clone()
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder {
|
|
||||||
PropsBuilder::from(self.props.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### on
|
fn on(&mut self, ev: Event) -> Msg {
|
||||||
///
|
|
||||||
/// Handle input event and update internal states
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
|
||||||
// Match event
|
// Match event
|
||||||
if let InputEvent::Key(key) = ev {
|
if let Event::Key(key) = ev {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
// Update states
|
// Update states
|
||||||
@@ -271,25 +281,14 @@ impl Component for LogBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get_value
|
fn get_state(&self) -> Payload {
|
||||||
///
|
Payload::One(Value::Usize(self.states.get_list_index()))
|
||||||
/// Return component value. File list return index
|
|
||||||
fn get_value(&self) -> Payload {
|
|
||||||
Payload::Unsigned(self.states.get_list_index())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component; basically remove focus
|
|
||||||
fn blur(&mut self) {
|
fn blur(&mut self) {
|
||||||
self.states.focus = false;
|
self.states.focus = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component; basically give focus
|
|
||||||
fn active(&mut self) {
|
fn active(&mut self) {
|
||||||
self.states.focus = true;
|
self.states.focus = true;
|
||||||
}
|
}
|
||||||
@@ -299,16 +298,20 @@ impl Component for LogBox {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan};
|
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use pretty_assertions::assert_eq;
|
||||||
use tui::style::Color;
|
use tuirealm::event::{KeyCode, KeyEvent};
|
||||||
|
use tuirealm::props::{TableBuilder, TextSpan};
|
||||||
|
use tuirealm::tui::style::Color;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ui_layout_components_logbox() {
|
fn test_ui_components_logbox() {
|
||||||
let mut component: LogBox = LogBox::new(
|
let mut component: LogBox = LogBox::new(
|
||||||
PropsBuilder::default()
|
LogboxPropsBuilder::default()
|
||||||
.with_texts(TextParts::table(
|
.hidden()
|
||||||
|
.visible()
|
||||||
|
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||||
|
.with_log(
|
||||||
Some(String::from("Log")),
|
Some(String::from("Log")),
|
||||||
TableBuilder::default()
|
TableBuilder::default()
|
||||||
.add_col(TextSpan::from("12:29"))
|
.add_col(TextSpan::from("12:29"))
|
||||||
@@ -317,9 +320,15 @@ mod tests {
|
|||||||
.add_col(TextSpan::from("12:38"))
|
.add_col(TextSpan::from("12:38"))
|
||||||
.add_col(TextSpan::from("system alive"))
|
.add_col(TextSpan::from("system alive"))
|
||||||
.build(),
|
.build(),
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
assert_eq!(component.props.visible, true);
|
||||||
|
assert_eq!(
|
||||||
|
component.props.texts.title.as_ref().unwrap().as_str(),
|
||||||
|
"Log"
|
||||||
|
);
|
||||||
|
assert_eq!(component.props.texts.table.as_ref().unwrap().len(), 2);
|
||||||
// Verify states
|
// Verify states
|
||||||
assert_eq!(component.states.list_index, 0);
|
assert_eq!(component.states.list_index, 0);
|
||||||
assert_eq!(component.states.list_len, 2);
|
assert_eq!(component.states.list_len, 2);
|
||||||
@@ -330,17 +339,18 @@ mod tests {
|
|||||||
component.blur();
|
component.blur();
|
||||||
assert_eq!(component.states.focus, false);
|
assert_eq!(component.states.focus, false);
|
||||||
// Update
|
// Update
|
||||||
let props = component.get_props().with_foreground(Color::Red).build();
|
let props = LogboxPropsBuilder::from(component.get_props())
|
||||||
|
.hidden()
|
||||||
|
.build();
|
||||||
assert_eq!(component.update(props), Msg::None);
|
assert_eq!(component.update(props), Msg::None);
|
||||||
assert_eq!(component.props.foreground, Color::Red);
|
assert_eq!(component.props.visible, false);
|
||||||
// Increment list index
|
// Increment list index
|
||||||
component.states.list_index += 1;
|
component.states.list_index += 1;
|
||||||
assert_eq!(component.states.list_index, 1);
|
assert_eq!(component.states.list_index, 1);
|
||||||
// Update
|
// Update
|
||||||
component.update(
|
component.update(
|
||||||
component
|
LogboxPropsBuilder::from(component.get_props())
|
||||||
.get_props()
|
.with_log(
|
||||||
.with_texts(TextParts::table(
|
|
||||||
Some(String::from("Log")),
|
Some(String::from("Log")),
|
||||||
TableBuilder::default()
|
TableBuilder::default()
|
||||||
.add_col(TextSpan::from("12:29"))
|
.add_col(TextSpan::from("12:29"))
|
||||||
@@ -352,49 +362,49 @@ mod tests {
|
|||||||
.add_col(TextSpan::from("12:41"))
|
.add_col(TextSpan::from("12:41"))
|
||||||
.add_col(TextSpan::from("system is going down for REBOOT"))
|
.add_col(TextSpan::from("system is going down for REBOOT"))
|
||||||
.build(),
|
.build(),
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
// Verify states
|
// Verify states
|
||||||
assert_eq!(component.states.list_index, 0); // Last item
|
assert_eq!(component.states.list_index, 0); // Last item
|
||||||
assert_eq!(component.states.list_len, 3);
|
assert_eq!(component.states.list_len, 3);
|
||||||
// get value
|
// get value
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(0));
|
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
|
||||||
// RenderData
|
// RenderData
|
||||||
assert_eq!(component.states.list_index, 0);
|
assert_eq!(component.states.list_index, 0);
|
||||||
// Set cursor to 0
|
// Set cursor to 0
|
||||||
component.states.list_index = 0;
|
component.states.list_index = 0;
|
||||||
// Handle inputs
|
// Handle inputs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Up))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
|
||||||
Msg::None
|
Msg::None
|
||||||
);
|
);
|
||||||
// Index should be incremented
|
// Index should be incremented
|
||||||
assert_eq!(component.states.list_index, 1);
|
assert_eq!(component.states.list_index, 1);
|
||||||
// Index should be decremented
|
// Index should be decremented
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Down))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||||
Msg::None
|
Msg::None
|
||||||
);
|
);
|
||||||
// Index should be incremented
|
// Index should be incremented
|
||||||
assert_eq!(component.states.list_index, 0);
|
assert_eq!(component.states.list_index, 0);
|
||||||
// Index should be 2
|
// Index should be 2
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageUp))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
|
||||||
Msg::None
|
Msg::None
|
||||||
);
|
);
|
||||||
// Index should be incremented
|
// Index should be incremented
|
||||||
assert_eq!(component.states.list_index, 2);
|
assert_eq!(component.states.list_index, 2);
|
||||||
// Index should be 0
|
// Index should be 0
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageDown))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
|
||||||
Msg::None
|
Msg::None
|
||||||
);
|
);
|
||||||
// Index should be incremented
|
// Index should be incremented
|
||||||
assert_eq!(component.states.list_index, 0);
|
assert_eq!(component.states.list_index, 0);
|
||||||
// On key
|
// On key
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||||
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## Components
|
//! ## Components
|
||||||
//!
|
//!
|
||||||
//! `Components` is the module which contains the definitions for all the GUI components for TermSCP
|
//! `Components` is the module which contains the definitions for all the GUI components for termscp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MIT License
|
* MIT License
|
||||||
@@ -25,17 +25,8 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// imports
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
|
|
||||||
|
|
||||||
// exports
|
// exports
|
||||||
pub mod bookmark_list;
|
pub mod bookmark_list;
|
||||||
pub mod file_list;
|
pub mod file_list;
|
||||||
pub mod input;
|
|
||||||
pub mod logbox;
|
pub mod logbox;
|
||||||
pub mod msgbox;
|
pub mod msgbox;
|
||||||
pub mod progress_bar;
|
|
||||||
pub mod radio_group;
|
|
||||||
pub mod table;
|
|
||||||
pub mod text;
|
|
||||||
pub mod title;
|
|
||||||
@@ -27,16 +27,99 @@
|
|||||||
*/
|
*/
|
||||||
// deps
|
// deps
|
||||||
extern crate textwrap;
|
extern crate textwrap;
|
||||||
|
extern crate tuirealm;
|
||||||
// locals
|
// locals
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
|
||||||
use crate::utils::fmt::align_text_center;
|
use crate::utils::fmt::align_text_center;
|
||||||
// ext
|
// ext
|
||||||
use tui::{
|
use tuirealm::components::utils::{get_block, use_or_default_styles};
|
||||||
|
use tuirealm::event::Event;
|
||||||
|
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
|
||||||
|
use tuirealm::tui::{
|
||||||
layout::{Corner, Rect},
|
layout::{Corner, Rect},
|
||||||
style::{Color, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Span, Spans},
|
text::{Span, Spans},
|
||||||
widgets::{Block, BorderType, List, ListItem},
|
widgets::{BorderType, Borders, List, ListItem},
|
||||||
};
|
};
|
||||||
|
use tuirealm::{Canvas, Component, Msg, Payload};
|
||||||
|
|
||||||
|
// -- Props
|
||||||
|
|
||||||
|
pub struct MsgBoxPropsBuilder {
|
||||||
|
props: Option<Props>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MsgBoxPropsBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
MsgBoxPropsBuilder {
|
||||||
|
props: Some(Props::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropsBuilder for MsgBoxPropsBuilder {
|
||||||
|
fn build(&mut self) -> Props {
|
||||||
|
self.props.take().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hidden(&mut self) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.visible = false;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible(&mut self) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.visible = true;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Props> for MsgBoxPropsBuilder {
|
||||||
|
fn from(props: Props) -> Self {
|
||||||
|
MsgBoxPropsBuilder { props: Some(props) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MsgBoxPropsBuilder {
|
||||||
|
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.foreground = color;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bold(&mut self) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.modifiers |= Modifier::BOLD;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_borders(
|
||||||
|
&mut self,
|
||||||
|
borders: Borders,
|
||||||
|
variant: BorderType,
|
||||||
|
color: Color,
|
||||||
|
) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.borders = BordersProps {
|
||||||
|
borders,
|
||||||
|
variant,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_texts(&mut self, title: Option<String>, texts: Vec<TextSpan>) -> &mut Self {
|
||||||
|
if let Some(props) = self.props.as_mut() {
|
||||||
|
props.texts = TextParts::new(title, Some(texts));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -- component
|
// -- component
|
||||||
|
|
||||||
@@ -54,56 +137,36 @@ impl MsgBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Component for MsgBox {
|
impl Component for MsgBox {
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
|
||||||
/// If focused, cursor is also set (if supported by widget)
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
#[cfg(not(tarpaulin_include))]
|
||||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
fn render(&self, render: &mut Canvas, area: Rect) {
|
||||||
// Make a Span
|
// Make a Span
|
||||||
if self.props.visible {
|
if self.props.visible {
|
||||||
let lines: Vec<ListItem> = match self.props.texts.rows.as_ref() {
|
let lines: Vec<ListItem> = match self.props.texts.spans.as_ref() {
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
Some(rows) => {
|
Some(rows) => {
|
||||||
let mut lines: Vec<ListItem> = Vec::new();
|
let mut lines: Vec<ListItem> = Vec::new();
|
||||||
for line in rows.iter() {
|
for line in rows.iter() {
|
||||||
// Keep line color, or use default
|
// Keep line color, or use default
|
||||||
let line_fg: Color = match line.fg {
|
let (fg, bg, modifiers) = use_or_default_styles(&self.props, line);
|
||||||
Color::Reset => self.props.foreground,
|
|
||||||
_ => line.fg,
|
|
||||||
};
|
|
||||||
let line_bg: Color = match line.bg {
|
|
||||||
Color::Reset => self.props.background,
|
|
||||||
_ => line.bg,
|
|
||||||
};
|
|
||||||
let message_row =
|
let message_row =
|
||||||
textwrap::wrap(line.content.as_str(), area.width as usize);
|
textwrap::wrap(line.content.as_str(), area.width as usize);
|
||||||
for msg in message_row.iter() {
|
for msg in message_row.iter() {
|
||||||
lines.push(ListItem::new(Spans::from(vec![Span::styled(
|
lines.push(ListItem::new(Spans::from(vec![Span::styled(
|
||||||
align_text_center(msg, area.width),
|
align_text_center(msg, area.width),
|
||||||
Style::default()
|
Style::default().add_modifier(modifiers).fg(fg).bg(bg),
|
||||||
.add_modifier(line.get_modifiers())
|
|
||||||
.fg(line_fg)
|
|
||||||
.bg(line_bg),
|
|
||||||
)])));
|
)])));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let title: String = match self.props.texts.title.as_ref() {
|
|
||||||
Some(t) => t.clone(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
render.render_widget(
|
render.render_widget(
|
||||||
List::new(lines)
|
List::new(lines)
|
||||||
.block(
|
.block(get_block(
|
||||||
Block::default()
|
&self.props.borders,
|
||||||
.borders(self.props.borders)
|
&self.props.texts.title,
|
||||||
.border_style(Style::default().fg(self.props.foreground))
|
true,
|
||||||
.border_type(BorderType::Rounded)
|
))
|
||||||
.title(title),
|
|
||||||
)
|
|
||||||
.start_corner(Corner::TopLeft)
|
.start_corner(Corner::TopLeft)
|
||||||
.style(
|
.style(
|
||||||
Style::default()
|
Style::default()
|
||||||
@@ -115,59 +178,31 @@ impl Component for MsgBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
fn update(&mut self, props: Props) -> Msg {
|
||||||
self.props = props;
|
self.props = props;
|
||||||
// Return None
|
// Return None
|
||||||
Msg::None
|
Msg::None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get_props
|
fn get_props(&self) -> Props {
|
||||||
///
|
self.props.clone()
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder {
|
|
||||||
PropsBuilder::from(self.props.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### on
|
fn on(&mut self, ev: Event) -> Msg {
|
||||||
///
|
|
||||||
/// Handle input event and update internal states.
|
|
||||||
/// Returns a Msg to the view.
|
|
||||||
/// Returns always None, since cannot have any focus
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
|
||||||
// Return key
|
// Return key
|
||||||
if let InputEvent::Key(key) = ev {
|
if let Event::Key(key) = ev {
|
||||||
Msg::OnKey(key)
|
Msg::OnKey(key)
|
||||||
} else {
|
} else {
|
||||||
Msg::None
|
Msg::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get_value
|
fn get_state(&self) -> Payload {
|
||||||
///
|
|
||||||
/// Get current value from component
|
|
||||||
/// For this component returns always None
|
|
||||||
fn get_value(&self) -> Payload {
|
|
||||||
Payload::None
|
Payload::None
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component
|
|
||||||
fn blur(&mut self) {}
|
fn blur(&mut self) {}
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component
|
|
||||||
fn active(&mut self) {}
|
fn active(&mut self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,39 +210,53 @@ impl Component for MsgBox {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::ui::layout::props::{TextParts, TextSpan, TextSpanBuilder};
|
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use pretty_assertions::assert_eq;
|
||||||
use tui::style::Color;
|
use tuirealm::event::{KeyCode, KeyEvent};
|
||||||
|
use tuirealm::props::{TextSpan, TextSpanBuilder};
|
||||||
|
use tuirealm::tui::style::Color;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ui_layout_components_msgbox() {
|
fn test_ui_components_msgbox() {
|
||||||
let mut component: MsgBox = MsgBox::new(
|
let mut component: MsgBox = MsgBox::new(
|
||||||
PropsBuilder::default()
|
MsgBoxPropsBuilder::default()
|
||||||
.with_texts(TextParts::new(
|
.hidden()
|
||||||
|
.visible()
|
||||||
|
.with_foreground(Color::Red)
|
||||||
|
.bold()
|
||||||
|
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
|
||||||
|
.with_texts(
|
||||||
None,
|
None,
|
||||||
Some(vec![
|
vec![
|
||||||
TextSpan::from("Press "),
|
TextSpan::from("Press "),
|
||||||
TextSpanBuilder::new("<ESC>")
|
TextSpanBuilder::new("<ESC>")
|
||||||
.with_foreground(Color::Cyan)
|
.with_foreground(Color::Cyan)
|
||||||
.bold()
|
.bold()
|
||||||
.build(),
|
.build(),
|
||||||
TextSpan::from(" to quit"),
|
TextSpan::from(" to quit"),
|
||||||
]),
|
],
|
||||||
))
|
)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
assert_eq!(component.props.foreground, Color::Red);
|
||||||
|
assert!(component.props.modifiers.intersects(Modifier::BOLD));
|
||||||
|
assert_eq!(component.props.visible, true);
|
||||||
|
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 3);
|
||||||
component.active();
|
component.active();
|
||||||
component.blur();
|
component.blur();
|
||||||
// Update
|
// Update
|
||||||
let props = component.get_props().with_foreground(Color::Red).build();
|
let props = MsgBoxPropsBuilder::from(component.get_props())
|
||||||
|
.hidden()
|
||||||
|
.with_foreground(Color::Yellow)
|
||||||
|
.build();
|
||||||
assert_eq!(component.update(props), Msg::None);
|
assert_eq!(component.update(props), Msg::None);
|
||||||
assert_eq!(component.props.foreground, Color::Red);
|
assert_eq!(component.props.visible, false);
|
||||||
|
assert_eq!(component.props.foreground, Color::Yellow);
|
||||||
// Get value
|
// Get value
|
||||||
assert_eq!(component.get_value(), Payload::None);
|
assert_eq!(component.get_state(), Payload::None);
|
||||||
// Event
|
// Event
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::Delete))),
|
||||||
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -27,13 +27,12 @@
|
|||||||
*/
|
*/
|
||||||
// Dependencies
|
// Dependencies
|
||||||
extern crate crossterm;
|
extern crate crossterm;
|
||||||
extern crate tui;
|
extern crate tuirealm;
|
||||||
|
|
||||||
// Locals
|
// Locals
|
||||||
use super::input::InputHandler;
|
use super::input::InputHandler;
|
||||||
use super::store::Store;
|
use super::store::Store;
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
use crate::host::Localhost;
|
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
@@ -42,14 +41,13 @@ use crossterm::execute;
|
|||||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||||
use std::io::{stdout, Stdout};
|
use std::io::{stdout, Stdout};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tui::backend::CrosstermBackend;
|
use tuirealm::tui::backend::CrosstermBackend;
|
||||||
use tui::Terminal;
|
use tuirealm::tui::Terminal;
|
||||||
|
|
||||||
/// ## Context
|
/// ## Context
|
||||||
///
|
///
|
||||||
/// Context holds data structures used by the ui
|
/// Context holds data structures used by the ui
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
pub local: Localhost,
|
|
||||||
pub ft_params: Option<FileTransferParams>,
|
pub ft_params: Option<FileTransferParams>,
|
||||||
pub(crate) config_client: Option<ConfigClient>,
|
pub(crate) config_client: Option<ConfigClient>,
|
||||||
pub(crate) store: Store,
|
pub(crate) store: Store,
|
||||||
@@ -74,16 +72,11 @@ impl Context {
|
|||||||
/// ### new
|
/// ### new
|
||||||
///
|
///
|
||||||
/// Instantiates a new Context
|
/// Instantiates a new Context
|
||||||
pub fn new(
|
pub fn new(config_client: Option<ConfigClient>, error: Option<String>) -> Context {
|
||||||
local: Localhost,
|
|
||||||
config_client: Option<ConfigClient>,
|
|
||||||
error: Option<String>,
|
|
||||||
) -> Context {
|
|
||||||
// Create terminal
|
// Create terminal
|
||||||
let mut stdout = stdout();
|
let mut stdout = stdout();
|
||||||
assert!(execute!(stdout, EnterAlternateScreen).is_ok());
|
assert!(execute!(stdout, EnterAlternateScreen).is_ok());
|
||||||
Context {
|
Context {
|
||||||
local,
|
|
||||||
ft_params: None,
|
ft_params: None,
|
||||||
config_client,
|
config_client,
|
||||||
store: Store::init(),
|
store: Store::init(),
|
||||||
@@ -93,14 +86,12 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NOTE: in case is necessary
|
|
||||||
/// ### set_error
|
/// ### set_error
|
||||||
///
|
///
|
||||||
/// Set context error
|
/// Set context error
|
||||||
pub fn set_error(&mut self, err: String) {
|
pub fn set_error(&mut self, err: String) {
|
||||||
self.error = Some(err);
|
self.error = Some(err);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/// ### get_error
|
/// ### get_error
|
||||||
///
|
///
|
||||||
@@ -113,40 +104,45 @@ impl Context {
|
|||||||
///
|
///
|
||||||
/// Enter alternate screen (gui window)
|
/// Enter alternate screen (gui window)
|
||||||
pub fn enter_alternate_screen(&mut self) {
|
pub fn enter_alternate_screen(&mut self) {
|
||||||
let _ = execute!(
|
match execute!(
|
||||||
self.terminal.backend_mut(),
|
self.terminal.backend_mut(),
|
||||||
EnterAlternateScreen,
|
EnterAlternateScreen,
|
||||||
DisableMouseCapture
|
DisableMouseCapture
|
||||||
);
|
) {
|
||||||
|
Err(err) => error!("Failed to enter alternate screen: {}", err),
|
||||||
|
Ok(_) => info!("Entered alternate screen"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### leave_alternate_screen
|
/// ### leave_alternate_screen
|
||||||
///
|
///
|
||||||
/// Go back to normal screen (gui window)
|
/// Go back to normal screen (gui window)
|
||||||
pub fn leave_alternate_screen(&mut self) {
|
pub fn leave_alternate_screen(&mut self) {
|
||||||
let _ = execute!(
|
match execute!(
|
||||||
self.terminal.backend_mut(),
|
self.terminal.backend_mut(),
|
||||||
LeaveAlternateScreen,
|
LeaveAlternateScreen,
|
||||||
DisableMouseCapture
|
DisableMouseCapture
|
||||||
);
|
) {
|
||||||
|
Err(err) => error!("Failed to leave alternate screen: {}", err),
|
||||||
|
Ok(_) => info!("Left alternate screen"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### clear_screen
|
/// ### clear_screen
|
||||||
///
|
///
|
||||||
/// Clear terminal screen
|
/// Clear terminal screen
|
||||||
pub fn clear_screen(&mut self) {
|
pub fn clear_screen(&mut self) {
|
||||||
let _ = self.terminal.clear();
|
match self.terminal.clear() {
|
||||||
|
Err(err) => error!("Failed to clear screen: {}", err),
|
||||||
|
Ok(_) => info!("Cleared screen"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Context {
|
impl Drop for Context {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// Re-enable terminal stuff
|
// Re-enable terminal stuff
|
||||||
let _ = execute!(
|
self.leave_alternate_screen();
|
||||||
self.terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +164,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ui_context_ft_params() {
|
fn test_ui_context_ft_params() {
|
||||||
let params: FileTransferParams = FileTransferParams::default();
|
let params: FileTransferParams = FileTransferParams::default();
|
||||||
@@ -178,31 +176,23 @@ mod tests {
|
|||||||
assert!(params.password.is_none());
|
assert!(params.password.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
//use crate::filetransfer::sftp_transfer::SftpFileTransfer;
|
|
||||||
//use std::path::PathBuf;
|
|
||||||
|
|
||||||
/*
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ui_context_new() {
|
#[cfg(not(feature = "githubActions"))]
|
||||||
|
fn test_ui_context() {
|
||||||
// Prepare stuff
|
// Prepare stuff
|
||||||
Context::new(
|
let mut ctx: Context = Context::new(None, Some(String::from("alles kaput")));
|
||||||
build_sftp_client(),
|
assert!(ctx.error.is_some());
|
||||||
Localhost::new(PathBuf::from("/")).ok().unwrap(),
|
assert_eq!(ctx.get_error().unwrap().as_str(), "alles kaput");
|
||||||
);
|
assert!(ctx.error.is_none());
|
||||||
|
assert!(ctx.get_error().is_none());
|
||||||
|
ctx.set_error(String::from("err"));
|
||||||
|
assert!(ctx.error.is_some());
|
||||||
|
assert!(ctx.get_error().is_some());
|
||||||
|
assert!(ctx.get_error().is_none());
|
||||||
|
// Try other methods
|
||||||
|
ctx.enter_alternate_screen();
|
||||||
|
ctx.clear_screen();
|
||||||
|
ctx.leave_alternate_screen();
|
||||||
|
drop(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_sftp_client() -> Box<dyn FileTransfer> {
|
|
||||||
let mut sftp_client: SftpFileTransfer = SftpFileTransfer::new();
|
|
||||||
// Connect to remote
|
|
||||||
assert!(sftp_client
|
|
||||||
.connect(
|
|
||||||
String::from("test.rebex.net"),
|
|
||||||
22,
|
|
||||||
Some(String::from("demo")),
|
|
||||||
Some(String::from("password"))
|
|
||||||
)
|
|
||||||
.is_ok());
|
|
||||||
Box::new(sftp_client)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
use crate::ui::layout::Msg;
|
use tuirealm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use tuirealm::Msg;
|
||||||
|
|
||||||
// -- Special keys
|
// -- Special keys
|
||||||
|
|
||||||
@@ -179,11 +179,11 @@ pub const MSG_KEY_CHAR_X: Msg = Msg::OnKey(KeyEvent {
|
|||||||
code: KeyCode::Char('x'),
|
code: KeyCode::Char('x'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
});
|
});
|
||||||
/*
|
|
||||||
pub const MSG_KEY_CHAR_Y: Msg = Msg::OnKey(KeyEvent {
|
pub const MSG_KEY_CHAR_Y: Msg = Msg::OnKey(KeyEvent {
|
||||||
code: KeyCode::Char('y'),
|
code: KeyCode::Char('y'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
});
|
});
|
||||||
|
/*
|
||||||
pub const MSG_KEY_CHAR_Z: Msg = Msg::OnKey(KeyEvent {
|
pub const MSG_KEY_CHAR_Z: Msg = Msg::OnKey(KeyEvent {
|
||||||
code: KeyCode::Char('z'),
|
code: KeyCode::Char('z'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
//! ## FileList
|
|
||||||
//!
|
|
||||||
//! `FileList` component renders a file list tab
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// locals
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
|
||||||
// ext
|
|
||||||
use crossterm::event::KeyCode;
|
|
||||||
use tui::{
|
|
||||||
layout::{Corner, Rect},
|
|
||||||
style::{Color, Style},
|
|
||||||
text::Span,
|
|
||||||
widgets::{Block, List, ListItem, ListState},
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- states
|
|
||||||
|
|
||||||
/// ## OwnStates
|
|
||||||
///
|
|
||||||
/// OwnStates contains states for this component
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct OwnStates {
|
|
||||||
list_index: usize, // Index of selected element in list
|
|
||||||
list_len: usize, // Length of file list
|
|
||||||
focus: bool, // Has focus?
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for OwnStates {
|
|
||||||
fn default() -> Self {
|
|
||||||
OwnStates {
|
|
||||||
list_index: 0,
|
|
||||||
list_len: 0,
|
|
||||||
focus: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OwnStates {
|
|
||||||
/// ### set_list_len
|
|
||||||
///
|
|
||||||
/// Set list length
|
|
||||||
pub fn set_list_len(&mut self, len: usize) {
|
|
||||||
self.list_len = len;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_list_index
|
|
||||||
///
|
|
||||||
/// Return current value for list index
|
|
||||||
pub fn get_list_index(&self) -> usize {
|
|
||||||
self.list_index
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### incr_list_index
|
|
||||||
///
|
|
||||||
/// Incremenet list index
|
|
||||||
pub fn incr_list_index(&mut self) {
|
|
||||||
// Check if index is at last element
|
|
||||||
if self.list_index + 1 < self.list_len {
|
|
||||||
self.list_index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### decr_list_index
|
|
||||||
///
|
|
||||||
/// Decrement list index
|
|
||||||
pub fn decr_list_index(&mut self) {
|
|
||||||
// Check if index is bigger than 0
|
|
||||||
if self.list_index > 0 {
|
|
||||||
self.list_index -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### fix_list_index
|
|
||||||
///
|
|
||||||
/// Keep index if possible, otherwise set to lenght - 1
|
|
||||||
pub fn fix_list_index(&mut self) {
|
|
||||||
if self.list_index >= self.list_len && self.list_len > 0 {
|
|
||||||
self.list_index = self.list_len - 1;
|
|
||||||
} else if self.list_len == 0 {
|
|
||||||
self.list_index = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Component
|
|
||||||
|
|
||||||
/// ## FileList
|
|
||||||
///
|
|
||||||
/// File list component
|
|
||||||
pub struct FileList {
|
|
||||||
props: Props,
|
|
||||||
states: OwnStates,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileList {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiates a new FileList starting from Props
|
|
||||||
/// The method also initializes the component states.
|
|
||||||
pub fn new(props: Props) -> Self {
|
|
||||||
// Initialize states
|
|
||||||
let mut states: OwnStates = OwnStates::default();
|
|
||||||
// Set list length
|
|
||||||
states.set_list_len(match &props.texts.rows {
|
|
||||||
Some(tokens) => tokens.len(),
|
|
||||||
None => 0,
|
|
||||||
});
|
|
||||||
FileList { props, states }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for FileList {
|
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
|
||||||
/// If focused, cursor is also set (if supported by widget)
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
|
||||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
|
||||||
if self.props.visible {
|
|
||||||
// Make list
|
|
||||||
let list_item: Vec<ListItem> = match self.props.texts.rows.as_ref() {
|
|
||||||
None => vec![],
|
|
||||||
Some(lines) => lines
|
|
||||||
.iter()
|
|
||||||
.map(|line| ListItem::new(Span::from(line.content.to_string())))
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
let (fg, bg): (Color, Color) = match self.states.focus {
|
|
||||||
true => (Color::Black, self.props.background),
|
|
||||||
false => (self.props.foreground, Color::Reset),
|
|
||||||
};
|
|
||||||
let title: String = match self.props.texts.title.as_ref() {
|
|
||||||
Some(t) => t.clone(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
// Render
|
|
||||||
let mut state: ListState = ListState::default();
|
|
||||||
state.select(Some(self.states.list_index));
|
|
||||||
render.render_stateful_widget(
|
|
||||||
List::new(list_item)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(self.props.borders)
|
|
||||||
.border_style(match self.states.focus {
|
|
||||||
true => Style::default().fg(self.props.foreground),
|
|
||||||
false => Style::default(),
|
|
||||||
})
|
|
||||||
.title(title),
|
|
||||||
)
|
|
||||||
.start_corner(Corner::TopLeft)
|
|
||||||
.highlight_style(
|
|
||||||
Style::default()
|
|
||||||
.bg(bg)
|
|
||||||
.fg(fg)
|
|
||||||
.add_modifier(self.props.get_modifiers()),
|
|
||||||
),
|
|
||||||
area,
|
|
||||||
&mut state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
|
||||||
self.props = props;
|
|
||||||
// re-Set list length
|
|
||||||
self.states.set_list_len(match &self.props.texts.rows {
|
|
||||||
Some(tokens) => tokens.len(),
|
|
||||||
None => 0,
|
|
||||||
});
|
|
||||||
// Fix list index
|
|
||||||
self.states.fix_list_index();
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_props
|
|
||||||
///
|
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder {
|
|
||||||
PropsBuilder::from(self.props.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### on
|
|
||||||
///
|
|
||||||
/// Handle input event and update internal states
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
|
||||||
// Match event
|
|
||||||
if let InputEvent::Key(key) = ev {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Down => {
|
|
||||||
// Update states
|
|
||||||
self.states.incr_list_index();
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
KeyCode::Up => {
|
|
||||||
// Update states
|
|
||||||
self.states.decr_list_index();
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
KeyCode::PageDown => {
|
|
||||||
// Update states
|
|
||||||
for _ in 0..8 {
|
|
||||||
self.states.incr_list_index();
|
|
||||||
}
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
KeyCode::PageUp => {
|
|
||||||
// Update states
|
|
||||||
for _ in 0..8 {
|
|
||||||
self.states.decr_list_index();
|
|
||||||
}
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
// Report event
|
|
||||||
Msg::OnSubmit(self.get_value())
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Return key event to activity
|
|
||||||
Msg::OnKey(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Unhandled event
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_value
|
|
||||||
///
|
|
||||||
/// Return component value. File list return index
|
|
||||||
fn get_value(&self) -> Payload {
|
|
||||||
Payload::Unsigned(self.states.get_list_index())
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component; basically remove focus
|
|
||||||
fn blur(&mut self) {
|
|
||||||
self.states.focus = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component; basically give focus
|
|
||||||
fn active(&mut self) {
|
|
||||||
self.states.focus = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::ui::layout::props::{TextParts, TextSpan};
|
|
||||||
|
|
||||||
use crossterm::event::KeyEvent;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_components_file_list() {
|
|
||||||
// Make component
|
|
||||||
let mut component: FileList = FileList::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(String::from("filelist")),
|
|
||||||
Some(vec![TextSpan::from("file1"), TextSpan::from("file2")]),
|
|
||||||
))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
// Verify states
|
|
||||||
assert_eq!(component.states.list_index, 0);
|
|
||||||
assert_eq!(component.states.list_len, 2);
|
|
||||||
assert_eq!(component.states.focus, false);
|
|
||||||
// Focus
|
|
||||||
component.active();
|
|
||||||
assert_eq!(component.states.focus, true);
|
|
||||||
component.blur();
|
|
||||||
assert_eq!(component.states.focus, false);
|
|
||||||
// Update
|
|
||||||
let props = component.get_props().with_foreground(Color::Red).build();
|
|
||||||
assert_eq!(component.update(props), Msg::None);
|
|
||||||
assert_eq!(component.props.foreground, Color::Red);
|
|
||||||
// Increment list index
|
|
||||||
component.states.list_index += 1;
|
|
||||||
assert_eq!(component.states.list_index, 1);
|
|
||||||
// Update
|
|
||||||
component.update(
|
|
||||||
component
|
|
||||||
.get_props()
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(String::from("filelist")),
|
|
||||||
Some(vec![
|
|
||||||
TextSpan::from("file1"),
|
|
||||||
TextSpan::from("file2"),
|
|
||||||
TextSpan::from("file3"),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
// Verify states
|
|
||||||
assert_eq!(component.states.list_index, 1); // Kept
|
|
||||||
assert_eq!(component.states.list_len, 3);
|
|
||||||
// get value
|
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(1));
|
|
||||||
// Render
|
|
||||||
assert_eq!(component.states.list_index, 1);
|
|
||||||
// Handle inputs
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Down))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
// Index should be incremented
|
|
||||||
assert_eq!(component.states.list_index, 2);
|
|
||||||
// Index should be decremented
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Up))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
// Index should be incremented
|
|
||||||
assert_eq!(component.states.list_index, 1);
|
|
||||||
// Index should be 2
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageDown))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
// Index should be incremented
|
|
||||||
assert_eq!(component.states.list_index, 2);
|
|
||||||
// Index should be 0
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageUp))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
// Index should be incremented
|
|
||||||
assert_eq!(component.states.list_index, 0);
|
|
||||||
// Enter
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
|
|
||||||
Msg::OnSubmit(Payload::Unsigned(0))
|
|
||||||
);
|
|
||||||
// On key
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
|
||||||
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,564 +0,0 @@
|
|||||||
//! ## Input
|
|
||||||
//!
|
|
||||||
//! `Input` component renders an input box
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// locals
|
|
||||||
use super::super::props::InputType;
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
|
|
||||||
// ext
|
|
||||||
use crossterm::event::{KeyCode, KeyModifiers};
|
|
||||||
use tui::{
|
|
||||||
layout::Rect,
|
|
||||||
style::Style,
|
|
||||||
widgets::{Block, BorderType, Paragraph},
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- states
|
|
||||||
|
|
||||||
/// ## OwnStates
|
|
||||||
///
|
|
||||||
/// OwnStates contains states for this component
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct OwnStates {
|
|
||||||
input: Vec<char>, // Current input
|
|
||||||
cursor: usize, // Input position
|
|
||||||
focus: bool, // Focus
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for OwnStates {
|
|
||||||
fn default() -> Self {
|
|
||||||
OwnStates {
|
|
||||||
input: Vec::new(),
|
|
||||||
cursor: 0,
|
|
||||||
focus: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OwnStates {
|
|
||||||
/// ### append
|
|
||||||
///
|
|
||||||
/// Append, if possible according to input type, the character to the input vec
|
|
||||||
pub fn append(&mut self, ch: char, itype: InputType, max_len: Option<usize>) {
|
|
||||||
// Check if max length has been reached
|
|
||||||
if self.input.len() < max_len.unwrap_or(usize::MAX) {
|
|
||||||
match itype {
|
|
||||||
InputType::Number => {
|
|
||||||
if ch.is_digit(10) {
|
|
||||||
// Must be digit
|
|
||||||
self.input.insert(self.cursor, ch);
|
|
||||||
// Increment cursor
|
|
||||||
self.cursor += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// No rule
|
|
||||||
self.input.insert(self.cursor, ch);
|
|
||||||
// Increment cursor
|
|
||||||
self.cursor += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### backspace
|
|
||||||
///
|
|
||||||
/// Delete element at cursor -1; then decrement cursor by 1
|
|
||||||
pub fn backspace(&mut self) {
|
|
||||||
if self.cursor > 0 && !self.input.is_empty() {
|
|
||||||
self.input.remove(self.cursor - 1);
|
|
||||||
// Decrement cursor
|
|
||||||
self.cursor -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### delete
|
|
||||||
///
|
|
||||||
/// Delete element at cursor
|
|
||||||
pub fn delete(&mut self) {
|
|
||||||
if self.cursor < self.input.len() {
|
|
||||||
self.input.remove(self.cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### incr_cursor
|
|
||||||
///
|
|
||||||
/// Increment cursor value by one if possible
|
|
||||||
pub fn incr_cursor(&mut self) {
|
|
||||||
if self.cursor < self.input.len() {
|
|
||||||
self.cursor += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### cursoro_at_begin
|
|
||||||
///
|
|
||||||
/// Place cursor at the begin of the input
|
|
||||||
pub fn cursor_at_begin(&mut self) {
|
|
||||||
self.cursor = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### cursor_at_end
|
|
||||||
///
|
|
||||||
/// Place cursor at the end of the input
|
|
||||||
pub fn cursor_at_end(&mut self) {
|
|
||||||
self.cursor = self.input.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### decr_cursor
|
|
||||||
///
|
|
||||||
/// Decrement cursor value by one if possible
|
|
||||||
pub fn decr_cursor(&mut self) {
|
|
||||||
if self.cursor > 0 {
|
|
||||||
self.cursor -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### render_value
|
|
||||||
///
|
|
||||||
/// Get value as string to render
|
|
||||||
pub fn render_value(&self, itype: InputType) -> String {
|
|
||||||
match itype {
|
|
||||||
InputType::Password => (0..self.input.len()).map(|_| '*').collect(),
|
|
||||||
_ => self.get_value(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_value
|
|
||||||
///
|
|
||||||
/// Get value as string
|
|
||||||
pub fn get_value(&self) -> String {
|
|
||||||
self.input.iter().collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Component
|
|
||||||
|
|
||||||
/// ## FileList
|
|
||||||
///
|
|
||||||
/// File list component
|
|
||||||
pub struct Input {
|
|
||||||
props: Props,
|
|
||||||
states: OwnStates,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Input {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiates a new Input starting from Props
|
|
||||||
/// The method also initializes the component states.
|
|
||||||
pub fn new(props: Props) -> Self {
|
|
||||||
// Initialize states
|
|
||||||
let mut states: OwnStates = OwnStates::default();
|
|
||||||
// Set state value from props
|
|
||||||
if let PropValue::Str(val) = props.value.clone() {
|
|
||||||
for ch in val.chars() {
|
|
||||||
states.append(ch, props.input_type, props.input_len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Input { props, states }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Input {
|
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
|
||||||
/// If focused, cursor is also set (if supported by widget)
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
|
||||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
|
||||||
if self.props.visible {
|
|
||||||
let title: String = match self.props.texts.title.as_ref() {
|
|
||||||
Some(t) => t.clone(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
let p: Paragraph = Paragraph::new(self.states.render_value(self.props.input_type))
|
|
||||||
.style(match self.states.focus {
|
|
||||||
true => Style::default().fg(self.props.foreground),
|
|
||||||
false => Style::default(),
|
|
||||||
})
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(self.props.borders)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.title(title),
|
|
||||||
);
|
|
||||||
render.render_widget(p, area);
|
|
||||||
// Set cursor, if focus
|
|
||||||
if self.states.focus {
|
|
||||||
let x: u16 = area.x + (self.states.cursor as u16) + 1;
|
|
||||||
render.set_cursor(x, area.y + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
|
||||||
self.props = props;
|
|
||||||
// Set value from props
|
|
||||||
if let PropValue::Str(val) = self.props.value.clone() {
|
|
||||||
self.states.input = Vec::new();
|
|
||||||
self.states.cursor = 0;
|
|
||||||
for ch in val.chars() {
|
|
||||||
self.states
|
|
||||||
.append(ch, self.props.input_type, self.props.input_len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_props
|
|
||||||
///
|
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder {
|
|
||||||
// Make properties with value from states
|
|
||||||
let mut props: Props = self.props.clone();
|
|
||||||
props.value = PropValue::Str(self.states.get_value());
|
|
||||||
PropsBuilder::from(props)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### on
|
|
||||||
///
|
|
||||||
/// Handle input event and update internal states.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
|
||||||
if let InputEvent::Key(key) = ev {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
// Backspace and None
|
|
||||||
self.states.backspace();
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
KeyCode::Delete => {
|
|
||||||
// Delete and None
|
|
||||||
self.states.delete();
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
KeyCode::Enter => Msg::OnSubmit(self.get_value()),
|
|
||||||
KeyCode::Left => {
|
|
||||||
// Move cursor left; msg None
|
|
||||||
self.states.decr_cursor();
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
KeyCode::Right => {
|
|
||||||
// Move cursor right; Msg None
|
|
||||||
self.states.incr_cursor();
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
KeyCode::End => {
|
|
||||||
// Cursor at last position
|
|
||||||
self.states.cursor_at_end();
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
KeyCode::Home => {
|
|
||||||
// Cursor at first positon
|
|
||||||
self.states.cursor_at_begin();
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
KeyCode::Char(ch) => {
|
|
||||||
// Check if modifiers is NOT CTRL OR ALT
|
|
||||||
if !key.modifiers.intersects(KeyModifiers::CONTROL)
|
|
||||||
&& !key.modifiers.intersects(KeyModifiers::ALT)
|
|
||||||
{
|
|
||||||
// Push char to input
|
|
||||||
self.states
|
|
||||||
.append(ch, self.props.input_type, self.props.input_len);
|
|
||||||
// Message none
|
|
||||||
Msg::None
|
|
||||||
} else {
|
|
||||||
// Return key
|
|
||||||
Msg::OnKey(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Msg::OnKey(key),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_value
|
|
||||||
///
|
|
||||||
/// Get current value from component
|
|
||||||
/// Returns the value as string or as a number based on the input value
|
|
||||||
fn get_value(&self) -> Payload {
|
|
||||||
match self.props.input_type {
|
|
||||||
InputType::Number => {
|
|
||||||
Payload::Unsigned(self.states.get_value().parse::<usize>().ok().unwrap_or(0))
|
|
||||||
}
|
|
||||||
_ => Payload::Text(self.states.get_value()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component; basically remove focus
|
|
||||||
fn blur(&mut self) {
|
|
||||||
self.states.focus = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component; basically give focus
|
|
||||||
fn active(&mut self) {
|
|
||||||
self.states.focus = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use crossterm::event::KeyEvent;
|
|
||||||
use tui::style::Color;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_components_input_text() {
|
|
||||||
// Instantiate Input with value
|
|
||||||
let mut component: Input = Input::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_input(InputType::Text)
|
|
||||||
.with_input_len(5)
|
|
||||||
.with_value(PropValue::Str(String::from("home")))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
// Verify initial state
|
|
||||||
assert_eq!(component.states.cursor, 4);
|
|
||||||
assert_eq!(component.states.input.len(), 4);
|
|
||||||
// Focus
|
|
||||||
assert_eq!(component.states.focus, false);
|
|
||||||
component.active();
|
|
||||||
assert_eq!(component.states.focus, true);
|
|
||||||
component.blur();
|
|
||||||
assert_eq!(component.states.focus, false);
|
|
||||||
// Update
|
|
||||||
let props = component.get_props().with_foreground(Color::Red).build();
|
|
||||||
assert_eq!(component.update(props), Msg::None);
|
|
||||||
assert_eq!(component.props.foreground, Color::Red);
|
|
||||||
// Get value
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("home")));
|
|
||||||
// RenderData
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 4);
|
|
||||||
assert_eq!(component.states.cursor, 4);
|
|
||||||
// Handle events
|
|
||||||
// Try key with ctrl
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::new(
|
|
||||||
KeyCode::Char('a'),
|
|
||||||
KeyModifiers::CONTROL
|
|
||||||
))),
|
|
||||||
Msg::OnKey(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)),
|
|
||||||
);
|
|
||||||
// String shouldn't have changed
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("home")));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 4);
|
|
||||||
assert_eq!(component.states.cursor, 4);
|
|
||||||
// Character
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('/')))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("home/")));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 5);
|
|
||||||
assert_eq!(component.states.cursor, 5);
|
|
||||||
// Verify max length (shouldn't push any character)
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("home/")));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 5);
|
|
||||||
assert_eq!(component.states.cursor, 5);
|
|
||||||
// Enter
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
|
|
||||||
Msg::OnSubmit(Payload::Text(String::from("home/")))
|
|
||||||
);
|
|
||||||
// Backspace
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("home")));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 4);
|
|
||||||
assert_eq!(component.states.cursor, 4);
|
|
||||||
// Check backspace at 0
|
|
||||||
component.states.input = vec!['h'];
|
|
||||||
component.states.cursor = 1;
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("")));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 0);
|
|
||||||
assert_eq!(component.states.cursor, 0);
|
|
||||||
// Another one...
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("")));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 0);
|
|
||||||
assert_eq!(component.states.cursor, 0);
|
|
||||||
// See del behaviour here
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("")));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 0);
|
|
||||||
assert_eq!(component.states.cursor, 0);
|
|
||||||
// Check del behaviour
|
|
||||||
component.states.input = vec!['h', 'e'];
|
|
||||||
component.states.cursor = 1;
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("h")));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 1); // Shouldn't move
|
|
||||||
assert_eq!(component.states.cursor, 1);
|
|
||||||
// Another one (should do nothing)
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("h")));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 1); // Shouldn't move
|
|
||||||
assert_eq!(component.states.cursor, 1);
|
|
||||||
// Move cursor right
|
|
||||||
component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
|
|
||||||
component.states.cursor = 1;
|
|
||||||
component.props.input_len = Some(16); // Let's change length
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))), // between 'e' and 'l'
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 2); // Should increment
|
|
||||||
assert_eq!(component.states.cursor, 2);
|
|
||||||
// Put a character here
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Text(String::from("heallo")));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 3);
|
|
||||||
assert_eq!(component.states.cursor, 3);
|
|
||||||
// Move left
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 2); // Should decrement
|
|
||||||
assert_eq!(component.states.cursor, 2);
|
|
||||||
// Go at the end
|
|
||||||
component.states.cursor = 6;
|
|
||||||
// Move right
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 6); // Should stay
|
|
||||||
assert_eq!(component.states.cursor, 6);
|
|
||||||
// Move left
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 5); // Should decrement
|
|
||||||
assert_eq!(component.states.cursor, 5);
|
|
||||||
// Go at the beginning
|
|
||||||
component.states.cursor = 0;
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 0); // Should stay
|
|
||||||
assert_eq!(component.states.cursor, 0);
|
|
||||||
// End - begin
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::End))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.states.cursor, 6);
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Home))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.states.cursor, 0);
|
|
||||||
// Update value
|
|
||||||
component.update(
|
|
||||||
component
|
|
||||||
.get_props()
|
|
||||||
.with_value(PropValue::Str("new-value".to_string()))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
component.get_value(),
|
|
||||||
Payload::Text(String::from("new-value"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_components_input_number() {
|
|
||||||
// Instantiate Input with value
|
|
||||||
let mut component: Input = Input::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_input(InputType::Number)
|
|
||||||
.with_input_len(5)
|
|
||||||
.with_value(PropValue::Str(String::from("3000")))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
// Verify initial state
|
|
||||||
assert_eq!(component.states.cursor, 4);
|
|
||||||
assert_eq!(component.states.input.len(), 4);
|
|
||||||
// Push a non numeric value
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(3000));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 4);
|
|
||||||
assert_eq!(component.states.cursor, 4);
|
|
||||||
// Push a number
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('1')))),
|
|
||||||
Msg::None
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(30001));
|
|
||||||
//assert_eq!(component.render().unwrap().cursor, 5);
|
|
||||||
assert_eq!(component.states.cursor, 5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
//! ## ProgressBar
|
|
||||||
//!
|
|
||||||
//! `ProgressBar` component renders a progress bar
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// locals
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
|
|
||||||
// ext
|
|
||||||
use tui::{
|
|
||||||
layout::Rect,
|
|
||||||
style::Style,
|
|
||||||
widgets::{Block, Gauge},
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- component
|
|
||||||
|
|
||||||
pub struct ProgressBar {
|
|
||||||
props: Props,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProgressBar {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiate a new Progress Bar
|
|
||||||
pub fn new(props: Props) -> Self {
|
|
||||||
ProgressBar { props }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for ProgressBar {
|
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
|
||||||
/// If focused, cursor is also set (if supported by widget)
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
|
||||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
|
||||||
// Make a Span
|
|
||||||
if self.props.visible {
|
|
||||||
let title: String = match self.props.texts.title.as_ref() {
|
|
||||||
Some(t) => t.clone(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
// Text
|
|
||||||
let label: String = match self.props.texts.rows.as_ref() {
|
|
||||||
Some(rows) => match rows.get(0) {
|
|
||||||
Some(label) => label.content.clone(),
|
|
||||||
None => String::new(),
|
|
||||||
},
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
// Get percentage
|
|
||||||
let percentage: f64 = match self.props.value {
|
|
||||||
PropValue::Float(ratio) => ratio,
|
|
||||||
_ => 0.0,
|
|
||||||
};
|
|
||||||
// Make progress bar
|
|
||||||
render.render_widget(
|
|
||||||
Gauge::default()
|
|
||||||
.block(Block::default().borders(self.props.borders).title(title))
|
|
||||||
.gauge_style(
|
|
||||||
Style::default()
|
|
||||||
.fg(self.props.foreground)
|
|
||||||
.bg(self.props.background)
|
|
||||||
.add_modifier(self.props.get_modifiers()),
|
|
||||||
)
|
|
||||||
.label(label)
|
|
||||||
.ratio(percentage),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
|
||||||
self.props = props;
|
|
||||||
// Return None
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_props
|
|
||||||
///
|
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder {
|
|
||||||
PropsBuilder::from(self.props.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### on
|
|
||||||
///
|
|
||||||
/// Handle input event and update internal states.
|
|
||||||
/// Returns a Msg to the view.
|
|
||||||
/// Returns always None, since cannot have any focus
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
|
||||||
// Return key
|
|
||||||
if let InputEvent::Key(key) = ev {
|
|
||||||
Msg::OnKey(key)
|
|
||||||
} else {
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_value
|
|
||||||
///
|
|
||||||
/// Get current value from component
|
|
||||||
/// For this component returns always None
|
|
||||||
fn get_value(&self) -> Payload {
|
|
||||||
Payload::None
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component
|
|
||||||
fn blur(&mut self) {}
|
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component
|
|
||||||
fn active(&mut self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::ui::layout::props::{TextParts, TextSpan};
|
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
|
||||||
use tui::style::Color;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_components_progress_bar() {
|
|
||||||
let mut component: ProgressBar = ProgressBar::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(String::from("Uploading file...")),
|
|
||||||
Some(vec![TextSpan::from("96.5% - ETA 00:02 (4.2MB/s)")]),
|
|
||||||
))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
// Get value
|
|
||||||
assert_eq!(component.get_value(), Payload::None);
|
|
||||||
component.active();
|
|
||||||
component.blur();
|
|
||||||
// Update
|
|
||||||
let props = component.get_props().with_foreground(Color::Red).build();
|
|
||||||
assert_eq!(component.update(props), Msg::None);
|
|
||||||
assert_eq!(component.props.foreground, Color::Red);
|
|
||||||
// Event
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
|
||||||
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
//! ## RadioGroup
|
|
||||||
//!
|
|
||||||
//! `RadioGroup` component renders a radio group
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// locals
|
|
||||||
use super::super::props::TextSpan;
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
|
|
||||||
// ext
|
|
||||||
use crossterm::event::KeyCode;
|
|
||||||
use tui::{
|
|
||||||
layout::Rect,
|
|
||||||
style::{Color, Style},
|
|
||||||
text::Spans,
|
|
||||||
widgets::{Block, BorderType, Tabs},
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- states
|
|
||||||
|
|
||||||
/// ## OwnStates
|
|
||||||
///
|
|
||||||
/// OwnStates contains states for this component
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct OwnStates {
|
|
||||||
choice: usize, // Selected option
|
|
||||||
choices: Vec<String>, // Available choices
|
|
||||||
focus: bool, // has focus?
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for OwnStates {
|
|
||||||
fn default() -> Self {
|
|
||||||
OwnStates {
|
|
||||||
choice: 0,
|
|
||||||
choices: Vec::new(),
|
|
||||||
focus: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OwnStates {
|
|
||||||
/// ### next_choice
|
|
||||||
///
|
|
||||||
/// Move choice index to next choice
|
|
||||||
pub fn next_choice(&mut self) {
|
|
||||||
if self.choice + 1 < self.choices.len() {
|
|
||||||
self.choice += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### prev_choice
|
|
||||||
///
|
|
||||||
/// Move choice index to previous choice
|
|
||||||
pub fn prev_choice(&mut self) {
|
|
||||||
if self.choice > 0 {
|
|
||||||
self.choice -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### make_choices
|
|
||||||
///
|
|
||||||
/// Set OwnStates choices from a vector of text spans
|
|
||||||
pub fn make_choices(&mut self, spans: &[TextSpan]) {
|
|
||||||
self.choices = spans.iter().map(|x| x.content.clone()).collect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- component
|
|
||||||
|
|
||||||
/// ## RadioGroup
|
|
||||||
///
|
|
||||||
/// RadioGroup component represents a group of tabs to select from
|
|
||||||
pub struct RadioGroup {
|
|
||||||
props: Props,
|
|
||||||
states: OwnStates,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RadioGroup {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiate a new Radio Group component
|
|
||||||
pub fn new(props: Props) -> Self {
|
|
||||||
// Make states
|
|
||||||
let mut states: OwnStates = OwnStates::default();
|
|
||||||
// Update choices (vec of TextSpan to String)
|
|
||||||
states.make_choices(props.texts.rows.as_ref().unwrap_or(&Vec::new()));
|
|
||||||
// Get value
|
|
||||||
if let PropValue::Unsigned(choice) = props.value {
|
|
||||||
states.choice = choice;
|
|
||||||
}
|
|
||||||
RadioGroup { props, states }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for RadioGroup {
|
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
|
||||||
/// If focused, cursor is also set (if supported by widget)
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
|
||||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
|
||||||
if self.props.visible {
|
|
||||||
// Make choices
|
|
||||||
let choices: Vec<Spans> = self
|
|
||||||
.states
|
|
||||||
.choices
|
|
||||||
.iter()
|
|
||||||
.map(|x| Spans::from(x.clone()))
|
|
||||||
.collect();
|
|
||||||
// Make colors
|
|
||||||
let (bg, fg, block_color): (Color, Color, Color) = match &self.states.focus {
|
|
||||||
true => (
|
|
||||||
self.props.foreground,
|
|
||||||
self.props.background,
|
|
||||||
self.props.foreground,
|
|
||||||
),
|
|
||||||
false => (Color::Reset, self.props.foreground, Color::Reset),
|
|
||||||
};
|
|
||||||
let title: String = match &self.props.texts.title {
|
|
||||||
Some(t) => t.clone(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
render.render_widget(
|
|
||||||
Tabs::new(choices)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(self.props.borders)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.style(Style::default())
|
|
||||||
.title(title),
|
|
||||||
)
|
|
||||||
.select(self.states.choice)
|
|
||||||
.style(Style::default().fg(block_color))
|
|
||||||
.highlight_style(
|
|
||||||
Style::default()
|
|
||||||
.add_modifier(self.props.get_modifiers())
|
|
||||||
.fg(fg)
|
|
||||||
.bg(bg),
|
|
||||||
),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
|
||||||
// Reset choices
|
|
||||||
self.states
|
|
||||||
.make_choices(props.texts.rows.as_ref().unwrap_or(&Vec::new()));
|
|
||||||
// Get value
|
|
||||||
if let PropValue::Unsigned(choice) = props.value {
|
|
||||||
self.states.choice = choice;
|
|
||||||
}
|
|
||||||
self.props = props;
|
|
||||||
// Msg none
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_props
|
|
||||||
///
|
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder {
|
|
||||||
PropsBuilder::from(self.props.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### on
|
|
||||||
///
|
|
||||||
/// Handle input event and update internal states.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
|
||||||
// Match event
|
|
||||||
if let InputEvent::Key(key) = ev {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Right => {
|
|
||||||
// Increment choice
|
|
||||||
self.states.next_choice();
|
|
||||||
// Return Msg On Change
|
|
||||||
Msg::OnChange(self.get_value())
|
|
||||||
}
|
|
||||||
KeyCode::Left => {
|
|
||||||
// Decrement choice
|
|
||||||
self.states.prev_choice();
|
|
||||||
// Return Msg On Change
|
|
||||||
Msg::OnChange(self.get_value())
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
// Return Submit
|
|
||||||
Msg::OnSubmit(self.get_value())
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Return key event to activity
|
|
||||||
Msg::OnKey(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Ignore event
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_value
|
|
||||||
///
|
|
||||||
/// Get current value from component
|
|
||||||
/// Returns the selected option
|
|
||||||
fn get_value(&self) -> Payload {
|
|
||||||
Payload::Unsigned(self.states.choice)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component; basically remove focus
|
|
||||||
fn blur(&mut self) {
|
|
||||||
self.states.focus = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component; basically give focus
|
|
||||||
fn active(&mut self) {
|
|
||||||
self.states.focus = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::ui::layout::props::{TextParts, TextSpan};
|
|
||||||
|
|
||||||
use crossterm::event::KeyEvent;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_components_radio() {
|
|
||||||
// Make component
|
|
||||||
let mut component: RadioGroup = RadioGroup::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(String::from("yes or no?")),
|
|
||||||
Some(vec![
|
|
||||||
TextSpan::from("Yes!"),
|
|
||||||
TextSpan::from("No"),
|
|
||||||
TextSpan::from("Maybe"),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
.with_value(PropValue::Unsigned(1))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
// Verify states
|
|
||||||
assert_eq!(component.states.choice, 1);
|
|
||||||
assert_eq!(component.states.choices.len(), 3);
|
|
||||||
// Focus
|
|
||||||
assert_eq!(component.states.focus, false);
|
|
||||||
component.active();
|
|
||||||
assert_eq!(component.states.focus, true);
|
|
||||||
component.blur();
|
|
||||||
assert_eq!(component.states.focus, false);
|
|
||||||
// Update
|
|
||||||
let props = component.get_props().with_foreground(Color::Red).build();
|
|
||||||
assert_eq!(component.update(props), Msg::None);
|
|
||||||
assert_eq!(component.props.foreground, Color::Red);
|
|
||||||
// Get value
|
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(1));
|
|
||||||
// Handle events
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
|
|
||||||
Msg::OnChange(Payload::Unsigned(0)),
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(0));
|
|
||||||
// Left again
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
|
|
||||||
Msg::OnChange(Payload::Unsigned(0)),
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(0));
|
|
||||||
// Right
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
|
|
||||||
Msg::OnChange(Payload::Unsigned(1)),
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(1));
|
|
||||||
// Right again
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
|
|
||||||
Msg::OnChange(Payload::Unsigned(2)),
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(2));
|
|
||||||
// Right again
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
|
|
||||||
Msg::OnChange(Payload::Unsigned(2)),
|
|
||||||
);
|
|
||||||
assert_eq!(component.get_value(), Payload::Unsigned(2));
|
|
||||||
// Submit
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
|
|
||||||
Msg::OnSubmit(Payload::Unsigned(2)),
|
|
||||||
);
|
|
||||||
// Any key
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
|
|
||||||
Msg::OnKey(KeyEvent::from(KeyCode::Char('a'))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
//! ## TextList
|
|
||||||
//!
|
|
||||||
//! `TextList` component renders a radio group
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// locals
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
|
||||||
// ext
|
|
||||||
use tui::{
|
|
||||||
layout::{Corner, Rect},
|
|
||||||
style::Style,
|
|
||||||
text::{Span, Spans},
|
|
||||||
widgets::{Block, BorderType, List, ListItem},
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- component
|
|
||||||
|
|
||||||
/// ## Table
|
|
||||||
///
|
|
||||||
/// Table is a table component. List n rows with n text span columns
|
|
||||||
pub struct Table {
|
|
||||||
props: Props,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Table {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiate a new Table component
|
|
||||||
pub fn new(props: Props) -> Self {
|
|
||||||
Table { props }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Table {
|
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
|
||||||
/// If focused, cursor is also set (if supported by widget)
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
|
||||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
|
||||||
// Make a Span
|
|
||||||
if self.props.visible {
|
|
||||||
let title: String = match self.props.texts.title.as_ref() {
|
|
||||||
Some(t) => t.clone(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
// Make list entries
|
|
||||||
let list_items: Vec<ListItem> = match self.props.texts.table.as_ref() {
|
|
||||||
None => Vec::new(),
|
|
||||||
Some(table) => table
|
|
||||||
.iter()
|
|
||||||
.map(|row| {
|
|
||||||
let columns: Vec<Span> = row
|
|
||||||
.iter()
|
|
||||||
.map(|col| {
|
|
||||||
Span::styled(
|
|
||||||
col.content.clone(),
|
|
||||||
Style::default()
|
|
||||||
.add_modifier(col.get_modifiers())
|
|
||||||
.fg(col.fg)
|
|
||||||
.bg(col.bg),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
ListItem::new(Spans::from(columns))
|
|
||||||
})
|
|
||||||
.collect(), // Make List item from TextSpan
|
|
||||||
};
|
|
||||||
// Make list
|
|
||||||
render.render_widget(
|
|
||||||
List::new(list_items)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(self.props.borders)
|
|
||||||
.border_style(Style::default())
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.title(title),
|
|
||||||
)
|
|
||||||
.start_corner(Corner::TopLeft),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
|
||||||
self.props = props;
|
|
||||||
// Return None
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_props
|
|
||||||
///
|
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder {
|
|
||||||
PropsBuilder::from(self.props.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### on
|
|
||||||
///
|
|
||||||
/// Handle input event and update internal states.
|
|
||||||
/// Returns a Msg to the view.
|
|
||||||
/// Returns always None, since cannot have any focus
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
|
||||||
// Return key
|
|
||||||
if let InputEvent::Key(key) = ev {
|
|
||||||
Msg::OnKey(key)
|
|
||||||
} else {
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_value
|
|
||||||
///
|
|
||||||
/// Get current value from component
|
|
||||||
/// For this component returns always None
|
|
||||||
fn get_value(&self) -> Payload {
|
|
||||||
Payload::None
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component
|
|
||||||
fn blur(&mut self) {}
|
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component
|
|
||||||
fn active(&mut self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan};
|
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
|
||||||
use tui::style::Color;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_components_table() {
|
|
||||||
let mut component: Table = Table::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_texts(TextParts::table(
|
|
||||||
Some(String::from("My data")),
|
|
||||||
TableBuilder::default()
|
|
||||||
.add_col(TextSpan::from("name"))
|
|
||||||
.add_col(TextSpan::from("age"))
|
|
||||||
.add_row()
|
|
||||||
.add_col(TextSpan::from("omar"))
|
|
||||||
.add_col(TextSpan::from("24"))
|
|
||||||
.build(),
|
|
||||||
))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
component.active();
|
|
||||||
component.blur();
|
|
||||||
// Update
|
|
||||||
let props = component.get_props().with_foreground(Color::Red).build();
|
|
||||||
assert_eq!(component.update(props), Msg::None);
|
|
||||||
assert_eq!(component.props.foreground, Color::Red);
|
|
||||||
// Get value
|
|
||||||
assert_eq!(component.get_value(), Payload::None);
|
|
||||||
// Event
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
|
||||||
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
//! ## Text
|
|
||||||
//!
|
|
||||||
//! `Text` component renders a simple readonly no event associated text
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// locals
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
|
||||||
// ext
|
|
||||||
use tui::{
|
|
||||||
layout::Rect,
|
|
||||||
style::{Color, Style},
|
|
||||||
text::{Span, Spans, Text as TuiText},
|
|
||||||
widgets::Paragraph,
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- component
|
|
||||||
|
|
||||||
pub struct Text {
|
|
||||||
props: Props,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Text {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiate a new Text component
|
|
||||||
pub fn new(props: Props) -> Self {
|
|
||||||
Text { props }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Text {
|
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
|
||||||
/// If focused, cursor is also set (if supported by widget)
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
|
||||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
|
||||||
// Make a Span
|
|
||||||
if self.props.visible {
|
|
||||||
let spans: Vec<Span> = match self.props.texts.rows.as_ref() {
|
|
||||||
None => Vec::new(),
|
|
||||||
Some(rows) => rows
|
|
||||||
.iter()
|
|
||||||
.map(|x| {
|
|
||||||
// Keep line color, or use default
|
|
||||||
let fg: Color = match x.fg {
|
|
||||||
Color::Reset => self.props.foreground,
|
|
||||||
_ => x.fg,
|
|
||||||
};
|
|
||||||
let bg: Color = match x.bg {
|
|
||||||
Color::Reset => self.props.background,
|
|
||||||
_ => x.bg,
|
|
||||||
};
|
|
||||||
Span::styled(
|
|
||||||
x.content.clone(),
|
|
||||||
Style::default()
|
|
||||||
.add_modifier(x.get_modifiers())
|
|
||||||
.fg(fg)
|
|
||||||
.bg(bg),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
// Make text
|
|
||||||
let mut text: TuiText = TuiText::from(Spans::from(spans));
|
|
||||||
// Apply style
|
|
||||||
text.patch_style(
|
|
||||||
Style::default()
|
|
||||||
//.add_modifier(self.props.get_modifiers())
|
|
||||||
//.fg(self.props.foreground) NOTE: don't style twice !!!
|
|
||||||
//.bg(self.props.background),
|
|
||||||
);
|
|
||||||
render.render_widget(Paragraph::new(text), area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
|
||||||
self.props = props;
|
|
||||||
// Return None
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_props
|
|
||||||
///
|
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder {
|
|
||||||
PropsBuilder::from(self.props.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### on
|
|
||||||
///
|
|
||||||
/// Handle input event and update internal states.
|
|
||||||
/// Returns a Msg to the view.
|
|
||||||
/// Returns always None, since cannot have any focus
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
|
||||||
// Return key
|
|
||||||
if let InputEvent::Key(key) = ev {
|
|
||||||
Msg::OnKey(key)
|
|
||||||
} else {
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_value
|
|
||||||
///
|
|
||||||
/// Get current value from component
|
|
||||||
/// For this component returns always None
|
|
||||||
fn get_value(&self) -> Payload {
|
|
||||||
Payload::None
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component
|
|
||||||
fn blur(&mut self) {}
|
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component
|
|
||||||
fn active(&mut self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::ui::layout::props::{TextParts, TextSpan, TextSpanBuilder};
|
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
|
||||||
use tui::style::Color;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_components_text() {
|
|
||||||
let mut component: Text = Text::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
None,
|
|
||||||
Some(vec![
|
|
||||||
TextSpan::from("Press "),
|
|
||||||
TextSpanBuilder::new("<ESC>")
|
|
||||||
.with_foreground(Color::Cyan)
|
|
||||||
.bold()
|
|
||||||
.build(),
|
|
||||||
TextSpan::from(" to quit"),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
component.active();
|
|
||||||
component.blur();
|
|
||||||
// Update
|
|
||||||
let props = component.get_props().with_foreground(Color::Red).build();
|
|
||||||
assert_eq!(component.update(props), Msg::None);
|
|
||||||
assert_eq!(component.props.foreground, Color::Red);
|
|
||||||
// Get value
|
|
||||||
assert_eq!(component.get_value(), Payload::None);
|
|
||||||
// Event
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
|
||||||
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
//! ## Title
|
|
||||||
//!
|
|
||||||
//! `Title` component renders a simple readonly no event associated title
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// locals
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
|
|
||||||
// ext
|
|
||||||
use tui::{layout::Rect, style::Style, widgets::Paragraph};
|
|
||||||
|
|
||||||
// -- component
|
|
||||||
|
|
||||||
pub struct Title {
|
|
||||||
props: Props,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Title {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiate a new Title component
|
|
||||||
pub fn new(props: Props) -> Self {
|
|
||||||
Title { props }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Title {
|
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
|
|
||||||
/// If focused, cursor is also set (if supported by widget)
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
|
||||||
fn render(&self, render: &mut Canvas, area: Rect) {
|
|
||||||
// Make a Span
|
|
||||||
if self.props.visible {
|
|
||||||
let title: String = match self.props.texts.title.as_ref() {
|
|
||||||
None => String::new(),
|
|
||||||
Some(t) => t.clone(),
|
|
||||||
};
|
|
||||||
render.render_widget(
|
|
||||||
Paragraph::new(title).style(
|
|
||||||
Style::default()
|
|
||||||
.fg(self.props.foreground)
|
|
||||||
.bg(self.props.background)
|
|
||||||
.add_modifier(self.props.get_modifiers()),
|
|
||||||
),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
|
||||||
self.props = props;
|
|
||||||
// Return None
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_props
|
|
||||||
///
|
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder {
|
|
||||||
PropsBuilder::from(self.props.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### on
|
|
||||||
///
|
|
||||||
/// Handle input event and update internal states.
|
|
||||||
/// Returns a Msg to the view.
|
|
||||||
/// Returns always None, since cannot have any focus
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg {
|
|
||||||
// Return key
|
|
||||||
if let InputEvent::Key(key) = ev {
|
|
||||||
Msg::OnKey(key)
|
|
||||||
} else {
|
|
||||||
Msg::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_value
|
|
||||||
///
|
|
||||||
/// Get current value from component
|
|
||||||
/// For this component returns always None
|
|
||||||
fn get_value(&self) -> Payload {
|
|
||||||
Payload::None
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component
|
|
||||||
fn blur(&mut self) {}
|
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component
|
|
||||||
fn active(&mut self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::ui::layout::props::TextParts;
|
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
|
||||||
use tui::style::Color;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_components_title() {
|
|
||||||
let mut component: Title = Title::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_texts(TextParts::new(Some(String::from("Title")), None))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
component.active();
|
|
||||||
component.blur();
|
|
||||||
// Update
|
|
||||||
let props = component.get_props().with_foreground(Color::Red).build();
|
|
||||||
assert_eq!(component.update(props), Msg::None);
|
|
||||||
assert_eq!(component.props.foreground, Color::Red);
|
|
||||||
// Get value
|
|
||||||
assert_eq!(component.get_value(), Payload::None);
|
|
||||||
// Event
|
|
||||||
assert_eq!(
|
|
||||||
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
|
|
||||||
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
//! ## Layout
|
|
||||||
//!
|
|
||||||
//! `Layout` is the module which provides components, view, state and properties to create layouts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// Modules
|
|
||||||
pub mod components;
|
|
||||||
pub mod props;
|
|
||||||
pub mod utils;
|
|
||||||
pub mod view;
|
|
||||||
|
|
||||||
// locals
|
|
||||||
use props::{PropValue, Props, PropsBuilder};
|
|
||||||
// ext
|
|
||||||
use crossterm::event::Event as InputEvent;
|
|
||||||
use crossterm::event::KeyEvent;
|
|
||||||
use std::io::Stdout;
|
|
||||||
use tui::backend::CrosstermBackend;
|
|
||||||
use tui::layout::Rect;
|
|
||||||
use tui::Frame;
|
|
||||||
|
|
||||||
type Backend = CrosstermBackend<Stdout>;
|
|
||||||
pub(crate) type Canvas<'a> = Frame<'a, Backend>;
|
|
||||||
|
|
||||||
// -- Msg
|
|
||||||
|
|
||||||
/// ## Msg
|
|
||||||
///
|
|
||||||
/// Msg is an enum returned after an event is raised for a certain component
|
|
||||||
/// Yep, I took inspiration from Elm.
|
|
||||||
#[derive(std::fmt::Debug, PartialEq, Eq)]
|
|
||||||
pub enum Msg {
|
|
||||||
OnSubmit(Payload),
|
|
||||||
OnChange(Payload),
|
|
||||||
OnKey(KeyEvent),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## Payload
|
|
||||||
///
|
|
||||||
/// Payload describes a component value
|
|
||||||
#[derive(std::fmt::Debug, PartialEq, Eq)]
|
|
||||||
pub enum Payload {
|
|
||||||
Text(String),
|
|
||||||
//Signed(isize),
|
|
||||||
Unsigned(usize),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Component
|
|
||||||
|
|
||||||
/// ## Component
|
|
||||||
///
|
|
||||||
/// Component is a trait which defines the behaviours for a Layout component.
|
|
||||||
/// All layout components must implement a method to render and one to update
|
|
||||||
pub trait Component {
|
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// Based on the current properties and states, renders the component in the provided area frame
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
|
||||||
fn render(&self, frame: &mut Canvas, area: Rect);
|
|
||||||
|
|
||||||
/// ### update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Properties should first be retrieved through `get_props` which creates a builder from
|
|
||||||
/// existing properties and then edited before calling update.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn update(&mut self, props: Props) -> Msg;
|
|
||||||
|
|
||||||
/// ### get_props
|
|
||||||
///
|
|
||||||
/// Returns a props builder starting from component properties.
|
|
||||||
/// This returns a prop builder in order to make easier to create
|
|
||||||
/// new properties for the element.
|
|
||||||
fn get_props(&self) -> PropsBuilder;
|
|
||||||
|
|
||||||
/// ### on
|
|
||||||
///
|
|
||||||
/// Handle input event and update internal states.
|
|
||||||
/// Returns a Msg to the view
|
|
||||||
fn on(&mut self, ev: InputEvent) -> Msg;
|
|
||||||
|
|
||||||
/// ### get_value
|
|
||||||
///
|
|
||||||
/// Get current value from component
|
|
||||||
fn get_value(&self) -> Payload;
|
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur component; basically remove focus
|
|
||||||
fn blur(&mut self);
|
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active component; basically give focus
|
|
||||||
fn active(&mut self);
|
|
||||||
}
|
|
||||||
@@ -1,779 +0,0 @@
|
|||||||
//! ## Props
|
|
||||||
//!
|
|
||||||
//! `Props` is the module which defines properties for layout components
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// ext
|
|
||||||
use tui::style::{Color, Modifier};
|
|
||||||
use tui::widgets::Borders;
|
|
||||||
|
|
||||||
// -- Props
|
|
||||||
|
|
||||||
/// ## Props
|
|
||||||
///
|
|
||||||
/// Props holds all the possible properties for a layout component
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Props {
|
|
||||||
// Values
|
|
||||||
pub visible: bool, // Is the element visible ON CREATE?
|
|
||||||
pub foreground: Color, // Foreground color
|
|
||||||
pub background: Color, // Background color
|
|
||||||
pub borders: Borders, // Borders
|
|
||||||
pub bold: bool, // Text bold
|
|
||||||
pub italic: bool, // Italic
|
|
||||||
pub underlined: bool, // Underlined
|
|
||||||
pub input_type: InputType, // Input type
|
|
||||||
pub input_len: Option<usize>, // max input len
|
|
||||||
pub texts: TextParts, // text parts
|
|
||||||
pub value: PropValue, // Initial value
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Props {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
// Values
|
|
||||||
visible: true,
|
|
||||||
foreground: Color::Reset,
|
|
||||||
background: Color::Reset,
|
|
||||||
borders: Borders::ALL,
|
|
||||||
bold: false,
|
|
||||||
italic: false,
|
|
||||||
underlined: false,
|
|
||||||
input_type: InputType::Text,
|
|
||||||
input_len: None,
|
|
||||||
texts: TextParts::default(),
|
|
||||||
value: PropValue::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Props {
|
|
||||||
/// ### get_modifiers
|
|
||||||
///
|
|
||||||
/// Get text modifiers from properties
|
|
||||||
pub fn get_modifiers(&self) -> Modifier {
|
|
||||||
Modifier::empty()
|
|
||||||
| (match self.bold {
|
|
||||||
true => Modifier::BOLD,
|
|
||||||
false => Modifier::empty(),
|
|
||||||
})
|
|
||||||
| (match self.italic {
|
|
||||||
true => Modifier::ITALIC,
|
|
||||||
false => Modifier::empty(),
|
|
||||||
})
|
|
||||||
| (match self.underlined {
|
|
||||||
true => Modifier::UNDERLINED,
|
|
||||||
false => Modifier::empty(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Props builder
|
|
||||||
|
|
||||||
/// ## PropsBuilder
|
|
||||||
///
|
|
||||||
/// Chain constructor for `Props`
|
|
||||||
pub struct PropsBuilder {
|
|
||||||
props: Option<Props>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl PropsBuilder {
|
|
||||||
/// ### build
|
|
||||||
///
|
|
||||||
/// Build Props from builder
|
|
||||||
/// Don't call this method twice for any reasons!
|
|
||||||
pub fn build(&mut self) -> Props {
|
|
||||||
self.props.take().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### hidden
|
|
||||||
///
|
|
||||||
/// Initialize props with visible set to False
|
|
||||||
pub fn hidden(&mut self) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.visible = false;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### visible
|
|
||||||
///
|
|
||||||
/// Initialize props with visible set to True
|
|
||||||
pub fn visible(&mut self) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.visible = true;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### with_foreground
|
|
||||||
///
|
|
||||||
/// Set foreground color for component
|
|
||||||
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.foreground = color;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### with_background
|
|
||||||
///
|
|
||||||
/// Set background color for component
|
|
||||||
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.background = color;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### with_borders
|
|
||||||
///
|
|
||||||
/// Set component borders style
|
|
||||||
pub fn with_borders(&mut self, borders: Borders) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.borders = borders;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### bold
|
|
||||||
///
|
|
||||||
/// Set bold property for component
|
|
||||||
pub fn bold(&mut self) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.bold = true;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### italic
|
|
||||||
///
|
|
||||||
/// Set italic property for component
|
|
||||||
pub fn italic(&mut self) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.italic = true;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### underlined
|
|
||||||
///
|
|
||||||
/// Set underlined property for component
|
|
||||||
pub fn underlined(&mut self) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.underlined = true;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### with_texts
|
|
||||||
///
|
|
||||||
/// Set texts for component
|
|
||||||
pub fn with_texts(&mut self, texts: TextParts) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.texts = texts;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### with_input
|
|
||||||
///
|
|
||||||
/// Set input type for component
|
|
||||||
pub fn with_input(&mut self, input_type: InputType) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.input_type = input_type;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### with_input_len
|
|
||||||
///
|
|
||||||
/// Set max input len
|
|
||||||
pub fn with_input_len(&mut self, len: usize) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.input_len = Some(len);
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### with_value
|
|
||||||
///
|
|
||||||
/// Set initial value for component
|
|
||||||
pub fn with_value(&mut self, value: PropValue) -> &mut Self {
|
|
||||||
if let Some(props) = self.props.as_mut() {
|
|
||||||
props.value = value;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Props> for PropsBuilder {
|
|
||||||
fn from(props: Props) -> Self {
|
|
||||||
PropsBuilder { props: Some(props) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PropsBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
PropsBuilder {
|
|
||||||
props: Some(Props::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Text parts
|
|
||||||
|
|
||||||
/// ## Table
|
|
||||||
///
|
|
||||||
/// Table represents a list of rows with a list of columns of text spans
|
|
||||||
pub type Table = Vec<Vec<TextSpan>>;
|
|
||||||
|
|
||||||
/// ## TextParts
|
|
||||||
///
|
|
||||||
/// TextParts holds optional component for the text displayed by a component
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TextParts {
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub rows: Option<Vec<TextSpan>>,
|
|
||||||
pub table: Option<Table>, // First vector is rows, inner vec is column
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextParts {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiates a new TextParts entity
|
|
||||||
pub fn new(title: Option<String>, rows: Option<Vec<TextSpan>>) -> Self {
|
|
||||||
TextParts {
|
|
||||||
title,
|
|
||||||
rows,
|
|
||||||
table: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### table
|
|
||||||
///
|
|
||||||
/// Instantiates a new TextParts as a Table
|
|
||||||
pub fn table(title: Option<String>, table: Table) -> Self {
|
|
||||||
TextParts {
|
|
||||||
title,
|
|
||||||
rows: None,
|
|
||||||
table: Some(table),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TextParts {
|
|
||||||
fn default() -> Self {
|
|
||||||
TextParts {
|
|
||||||
title: None,
|
|
||||||
rows: None,
|
|
||||||
table: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## TableBuilder
|
|
||||||
///
|
|
||||||
/// Table builder is a helper to make it easier to build text tables
|
|
||||||
pub struct TableBuilder {
|
|
||||||
table: Option<Table>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TableBuilder {
|
|
||||||
/// ### add_col
|
|
||||||
///
|
|
||||||
/// Add a column to the last row
|
|
||||||
pub fn add_col(&mut self, span: TextSpan) -> &mut Self {
|
|
||||||
if let Some(table) = self.table.as_mut() {
|
|
||||||
if let Some(row) = table.last_mut() {
|
|
||||||
row.push(span);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### add_row
|
|
||||||
///
|
|
||||||
/// Add a new row to the table
|
|
||||||
pub fn add_row(&mut self) -> &mut Self {
|
|
||||||
if let Some(table) = self.table.as_mut() {
|
|
||||||
table.push(vec![]);
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### build
|
|
||||||
///
|
|
||||||
/// Take table out of builder
|
|
||||||
/// Don't call this method twice for any reasons!
|
|
||||||
pub fn build(&mut self) -> Table {
|
|
||||||
self.table.take().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TableBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
TableBuilder {
|
|
||||||
table: Some(vec![vec![]]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### TextSpan
|
|
||||||
///
|
|
||||||
/// TextSpan is a "cell" of text with its attributes
|
|
||||||
#[derive(Clone, std::fmt::Debug)]
|
|
||||||
pub struct TextSpan {
|
|
||||||
pub content: String,
|
|
||||||
pub fg: Color,
|
|
||||||
pub bg: Color,
|
|
||||||
pub bold: bool,
|
|
||||||
pub italic: bool,
|
|
||||||
pub underlined: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for TextSpan {
|
|
||||||
fn from(txt: &str) -> Self {
|
|
||||||
TextSpan {
|
|
||||||
content: txt.to_string(),
|
|
||||||
fg: Color::Reset,
|
|
||||||
bg: Color::Reset,
|
|
||||||
bold: false,
|
|
||||||
italic: false,
|
|
||||||
underlined: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for TextSpan {
|
|
||||||
fn from(content: String) -> Self {
|
|
||||||
TextSpan {
|
|
||||||
content,
|
|
||||||
fg: Color::Reset,
|
|
||||||
bg: Color::Reset,
|
|
||||||
bold: false,
|
|
||||||
italic: false,
|
|
||||||
underlined: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextSpan {
|
|
||||||
/// ### get_modifiers
|
|
||||||
///
|
|
||||||
/// Get text modifiers from properties
|
|
||||||
pub fn get_modifiers(&self) -> Modifier {
|
|
||||||
Modifier::empty()
|
|
||||||
| (match self.bold {
|
|
||||||
true => Modifier::BOLD,
|
|
||||||
false => Modifier::empty(),
|
|
||||||
})
|
|
||||||
| (match self.italic {
|
|
||||||
true => Modifier::ITALIC,
|
|
||||||
false => Modifier::empty(),
|
|
||||||
})
|
|
||||||
| (match self.underlined {
|
|
||||||
true => Modifier::UNDERLINED,
|
|
||||||
false => Modifier::empty(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- TextSpan builder
|
|
||||||
|
|
||||||
/// ## TextSpanBuilder
|
|
||||||
///
|
|
||||||
/// TextSpanBuilder is a struct which helps building quickly a TextSpan
|
|
||||||
pub struct TextSpanBuilder {
|
|
||||||
text: Option<TextSpan>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl TextSpanBuilder {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiate a new TextSpanBuilder
|
|
||||||
pub fn new(text: &str) -> Self {
|
|
||||||
TextSpanBuilder {
|
|
||||||
text: Some(TextSpan::from(text)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### with_foreground
|
|
||||||
///
|
|
||||||
/// Set foreground for text span
|
|
||||||
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
|
||||||
if let Some(text) = self.text.as_mut() {
|
|
||||||
text.fg = color;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### with_background
|
|
||||||
///
|
|
||||||
/// Set background for text span
|
|
||||||
pub fn with_background(&mut self, color: Color) -> &mut Self {
|
|
||||||
if let Some(text) = self.text.as_mut() {
|
|
||||||
text.bg = color;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### italic
|
|
||||||
///
|
|
||||||
/// Set italic for text span
|
|
||||||
pub fn italic(&mut self) -> &mut Self {
|
|
||||||
if let Some(text) = self.text.as_mut() {
|
|
||||||
text.italic = true;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// ### bold
|
|
||||||
///
|
|
||||||
/// Set bold for text span
|
|
||||||
pub fn bold(&mut self) -> &mut Self {
|
|
||||||
if let Some(text) = self.text.as_mut() {
|
|
||||||
text.bold = true;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### underlined
|
|
||||||
///
|
|
||||||
/// Set underlined for text span
|
|
||||||
pub fn underlined(&mut self) -> &mut Self {
|
|
||||||
if let Some(text) = self.text.as_mut() {
|
|
||||||
text.underlined = true;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### build
|
|
||||||
///
|
|
||||||
/// Make TextSpan out of builder
|
|
||||||
/// Don't call this method twice for any reasons!
|
|
||||||
pub fn build(&mut self) -> TextSpan {
|
|
||||||
self.text.take().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Prop value
|
|
||||||
|
|
||||||
/// ### PropValue
|
|
||||||
///
|
|
||||||
/// PropValue describes a property initial value
|
|
||||||
#[derive(Clone, PartialEq, std::fmt::Debug)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum PropValue {
|
|
||||||
Str(String),
|
|
||||||
Unsigned(usize),
|
|
||||||
Signed(isize),
|
|
||||||
Float(f64),
|
|
||||||
Boolean(bool),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Input Type
|
|
||||||
|
|
||||||
/// ## InputType
|
|
||||||
///
|
|
||||||
/// Input type for text inputs
|
|
||||||
#[derive(Clone, Copy, PartialEq, std::fmt::Debug)]
|
|
||||||
pub enum InputType {
|
|
||||||
Text,
|
|
||||||
Number,
|
|
||||||
Password,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_props_default() {
|
|
||||||
let props: Props = Props::default();
|
|
||||||
assert_eq!(props.visible, true);
|
|
||||||
assert_eq!(props.background, Color::Reset);
|
|
||||||
assert_eq!(props.foreground, Color::Reset);
|
|
||||||
assert_eq!(props.borders, Borders::ALL);
|
|
||||||
assert_eq!(props.bold, false);
|
|
||||||
assert_eq!(props.italic, false);
|
|
||||||
assert_eq!(props.underlined, false);
|
|
||||||
assert!(props.texts.title.is_none());
|
|
||||||
assert_eq!(props.input_type, InputType::Text);
|
|
||||||
assert!(props.input_len.is_none());
|
|
||||||
assert_eq!(props.value, PropValue::None);
|
|
||||||
assert!(props.texts.rows.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_props_modifiers() {
|
|
||||||
// Make properties
|
|
||||||
let props: Props = PropsBuilder::default().bold().italic().underlined().build();
|
|
||||||
// Get modifiers
|
|
||||||
let modifiers: Modifier = props.get_modifiers();
|
|
||||||
assert!(modifiers.intersects(Modifier::BOLD));
|
|
||||||
assert!(modifiers.intersects(Modifier::ITALIC));
|
|
||||||
assert!(modifiers.intersects(Modifier::UNDERLINED));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_props_builder() {
|
|
||||||
let props: Props = PropsBuilder::default()
|
|
||||||
.hidden()
|
|
||||||
.with_background(Color::Blue)
|
|
||||||
.with_foreground(Color::Green)
|
|
||||||
.with_borders(Borders::BOTTOM)
|
|
||||||
.bold()
|
|
||||||
.italic()
|
|
||||||
.underlined()
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(String::from("hello")),
|
|
||||||
Some(vec![TextSpan::from("hey")]),
|
|
||||||
))
|
|
||||||
.with_input(InputType::Password)
|
|
||||||
.with_input_len(16)
|
|
||||||
.with_value(PropValue::Str(String::from("Hello")))
|
|
||||||
.build();
|
|
||||||
assert_eq!(props.background, Color::Blue);
|
|
||||||
assert_eq!(props.borders, Borders::BOTTOM);
|
|
||||||
assert_eq!(props.bold, true);
|
|
||||||
assert_eq!(props.foreground, Color::Green);
|
|
||||||
assert_eq!(props.italic, true);
|
|
||||||
assert_eq!(props.texts.title.as_ref().unwrap().as_str(), "hello");
|
|
||||||
assert_eq!(props.input_type, InputType::Password);
|
|
||||||
assert_eq!(*props.input_len.as_ref().unwrap(), 16);
|
|
||||||
if let PropValue::Str(s) = props.value {
|
|
||||||
assert_eq!(s.as_str(), "Hello");
|
|
||||||
} else {
|
|
||||||
panic!("Expected value to be a string");
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
props
|
|
||||||
.texts
|
|
||||||
.rows
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.get(0)
|
|
||||||
.unwrap()
|
|
||||||
.content
|
|
||||||
.as_str(),
|
|
||||||
"hey"
|
|
||||||
);
|
|
||||||
assert_eq!(props.underlined, true);
|
|
||||||
assert_eq!(props.visible, false);
|
|
||||||
let props: Props = PropsBuilder::default()
|
|
||||||
.visible()
|
|
||||||
.with_background(Color::Blue)
|
|
||||||
.with_foreground(Color::Green)
|
|
||||||
.bold()
|
|
||||||
.italic()
|
|
||||||
.underlined()
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(String::from("hello")),
|
|
||||||
Some(vec![TextSpan::from("hey")]),
|
|
||||||
))
|
|
||||||
.build();
|
|
||||||
assert_eq!(props.background, Color::Blue);
|
|
||||||
assert_eq!(props.bold, true);
|
|
||||||
assert_eq!(props.foreground, Color::Green);
|
|
||||||
assert_eq!(props.italic, true);
|
|
||||||
assert_eq!(props.texts.title.as_ref().unwrap().as_str(), "hello");
|
|
||||||
assert_eq!(
|
|
||||||
props
|
|
||||||
.texts
|
|
||||||
.rows
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.get(0)
|
|
||||||
.unwrap()
|
|
||||||
.content
|
|
||||||
.as_str(),
|
|
||||||
"hey"
|
|
||||||
);
|
|
||||||
assert_eq!(props.underlined, true);
|
|
||||||
assert_eq!(props.visible, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_ui_layout_props_build_twice() {
|
|
||||||
let mut builder: PropsBuilder = PropsBuilder::default();
|
|
||||||
let _ = builder.build();
|
|
||||||
builder
|
|
||||||
.hidden()
|
|
||||||
.with_background(Color::Blue)
|
|
||||||
.with_foreground(Color::Green)
|
|
||||||
.bold()
|
|
||||||
.italic()
|
|
||||||
.underlined()
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(String::from("hello")),
|
|
||||||
Some(vec![TextSpan::from("hey")]),
|
|
||||||
));
|
|
||||||
// Rebuild
|
|
||||||
let _ = builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_props_builder_from_props() {
|
|
||||||
let props: Props = PropsBuilder::default()
|
|
||||||
.hidden()
|
|
||||||
.with_background(Color::Blue)
|
|
||||||
.with_foreground(Color::Green)
|
|
||||||
.bold()
|
|
||||||
.italic()
|
|
||||||
.underlined()
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
Some(String::from("hello")),
|
|
||||||
Some(vec![TextSpan::from("hey")]),
|
|
||||||
))
|
|
||||||
.build();
|
|
||||||
// Ok, now make a builder from properties
|
|
||||||
let builder: PropsBuilder = PropsBuilder::from(props);
|
|
||||||
assert!(builder.props.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_props_text_parts_with_values() {
|
|
||||||
let parts: TextParts = TextParts::new(
|
|
||||||
Some(String::from("Hello world!")),
|
|
||||||
Some(vec![TextSpan::from("row1"), TextSpan::from("row2")]),
|
|
||||||
);
|
|
||||||
assert_eq!(parts.title.as_ref().unwrap().as_str(), "Hello world!");
|
|
||||||
assert_eq!(
|
|
||||||
parts
|
|
||||||
.rows
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.get(0)
|
|
||||||
.unwrap()
|
|
||||||
.content
|
|
||||||
.as_str(),
|
|
||||||
"row1"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parts
|
|
||||||
.rows
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.get(1)
|
|
||||||
.unwrap()
|
|
||||||
.content
|
|
||||||
.as_str(),
|
|
||||||
"row2"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_props_text_parts_default() {
|
|
||||||
let parts: TextParts = TextParts::default();
|
|
||||||
assert!(parts.title.is_none());
|
|
||||||
assert!(parts.rows.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_props_text_parts_table() {
|
|
||||||
let table: TextParts = TextParts::table(
|
|
||||||
Some(String::from("my data")),
|
|
||||||
TableBuilder::default()
|
|
||||||
.add_col(TextSpan::from("name"))
|
|
||||||
.add_col(TextSpan::from("age"))
|
|
||||||
.add_row()
|
|
||||||
.add_col(TextSpan::from("christian"))
|
|
||||||
.add_col(TextSpan::from("23"))
|
|
||||||
.add_row()
|
|
||||||
.add_col(TextSpan::from("omar"))
|
|
||||||
.add_col(TextSpan::from("25"))
|
|
||||||
.add_row()
|
|
||||||
.add_row()
|
|
||||||
.add_col(TextSpan::from("pippo"))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
// Verify table
|
|
||||||
assert_eq!(table.title.as_ref().unwrap().as_str(), "my data");
|
|
||||||
assert!(table.rows.is_none());
|
|
||||||
assert_eq!(table.table.as_ref().unwrap().len(), 5); // 5 rows
|
|
||||||
assert_eq!(table.table.as_ref().unwrap().get(0).unwrap().len(), 2); // 2 cols
|
|
||||||
assert_eq!(table.table.as_ref().unwrap().get(1).unwrap().len(), 2); // 2 cols
|
|
||||||
assert_eq!(
|
|
||||||
table
|
|
||||||
.table
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.get(1)
|
|
||||||
.unwrap()
|
|
||||||
.get(0)
|
|
||||||
.unwrap()
|
|
||||||
.content
|
|
||||||
.as_str(),
|
|
||||||
"christian"
|
|
||||||
); // check content
|
|
||||||
assert_eq!(table.table.as_ref().unwrap().get(2).unwrap().len(), 2); // 2 cols
|
|
||||||
assert_eq!(table.table.as_ref().unwrap().get(3).unwrap().len(), 0); // 0 cols
|
|
||||||
assert_eq!(table.table.as_ref().unwrap().get(4).unwrap().len(), 1); // 1 cols
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_props_text_span() {
|
|
||||||
// from str
|
|
||||||
let span: TextSpan = TextSpan::from("Hello!");
|
|
||||||
assert_eq!(span.content.as_str(), "Hello!");
|
|
||||||
assert_eq!(span.bold, false);
|
|
||||||
assert_eq!(span.fg, Color::Reset);
|
|
||||||
assert_eq!(span.bg, Color::Reset);
|
|
||||||
assert_eq!(span.italic, false);
|
|
||||||
assert_eq!(span.underlined, false);
|
|
||||||
// From String
|
|
||||||
let span: TextSpan = TextSpan::from(String::from("omar"));
|
|
||||||
assert_eq!(span.content.as_str(), "omar");
|
|
||||||
assert_eq!(span.bold, false);
|
|
||||||
assert_eq!(span.fg, Color::Reset);
|
|
||||||
assert_eq!(span.bg, Color::Reset);
|
|
||||||
assert_eq!(span.italic, false);
|
|
||||||
assert_eq!(span.underlined, false);
|
|
||||||
// With attributes
|
|
||||||
let span: TextSpan = TextSpanBuilder::new("Error")
|
|
||||||
.with_background(Color::Red)
|
|
||||||
.with_foreground(Color::Black)
|
|
||||||
.bold()
|
|
||||||
.italic()
|
|
||||||
.underlined()
|
|
||||||
.build();
|
|
||||||
assert_eq!(span.content.as_str(), "Error");
|
|
||||||
assert_eq!(span.bold, true);
|
|
||||||
assert_eq!(span.fg, Color::Black);
|
|
||||||
assert_eq!(span.bg, Color::Red);
|
|
||||||
assert_eq!(span.italic, true);
|
|
||||||
assert_eq!(span.underlined, true);
|
|
||||||
// Check modifiers
|
|
||||||
let modifiers: Modifier = span.get_modifiers();
|
|
||||||
assert!(modifiers.intersects(Modifier::BOLD));
|
|
||||||
assert!(modifiers.intersects(Modifier::ITALIC));
|
|
||||||
assert!(modifiers.intersects(Modifier::UNDERLINED));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
//! ## View
|
|
||||||
//!
|
|
||||||
//! `View` is the module which handles layout components
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// imports
|
|
||||||
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder, Rect};
|
|
||||||
// ext
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// ## View
|
|
||||||
///
|
|
||||||
/// View is the wrapper and manager for all the components.
|
|
||||||
/// A View is a container for all the components in a certain layout.
|
|
||||||
/// Each View can have only one focused component.
|
|
||||||
pub struct View {
|
|
||||||
components: HashMap<String, Box<dyn Component>>, // all the components in the view
|
|
||||||
focus: Option<String>, // Current active component
|
|
||||||
focus_stack: Vec<String>, // Focus stack; used to give focus in case the current element loses focus
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- view
|
|
||||||
|
|
||||||
impl View {
|
|
||||||
/// ### init
|
|
||||||
///
|
|
||||||
/// Initialize a new `View`
|
|
||||||
pub fn init() -> Self {
|
|
||||||
View {
|
|
||||||
components: HashMap::new(),
|
|
||||||
focus: None,
|
|
||||||
focus_stack: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- mount / umount
|
|
||||||
|
|
||||||
/// ### mount
|
|
||||||
///
|
|
||||||
/// Mount a new component in the view
|
|
||||||
pub fn mount(&mut self, id: &str, component: Box<dyn Component>) {
|
|
||||||
self.components.insert(id.to_string(), component);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### umount
|
|
||||||
///
|
|
||||||
/// Umount a component from the view.
|
|
||||||
/// If component has focus, blur component and remove it from the stack
|
|
||||||
pub fn umount(&mut self, id: &str) {
|
|
||||||
// Check if component has focus
|
|
||||||
if let Some(focus) = self.focus.as_ref() {
|
|
||||||
// If has focus, blur component
|
|
||||||
if focus == id {
|
|
||||||
self.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove component from focus stack
|
|
||||||
self.pop_from_stack(id);
|
|
||||||
self.components.remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- render
|
|
||||||
|
|
||||||
/// ### render
|
|
||||||
///
|
|
||||||
/// RenderData component with the provided id
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
|
||||||
pub fn render(&self, id: &str, frame: &mut Canvas, area: Rect) {
|
|
||||||
if let Some(component) = self.components.get(id) {
|
|
||||||
component.render(frame, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- props
|
|
||||||
|
|
||||||
/// ### get_props
|
|
||||||
///
|
|
||||||
/// Get component properties
|
|
||||||
pub fn get_props(&self, id: &str) -> Option<PropsBuilder> {
|
|
||||||
self.components.get(id).map(|cmp| cmp.get_props())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// update
|
|
||||||
///
|
|
||||||
/// Update component properties
|
|
||||||
/// Returns `None` if component doesn't exist
|
|
||||||
pub fn update(&mut self, id: &str, props: Props) -> Option<(String, Msg)> {
|
|
||||||
self.components
|
|
||||||
.get_mut(id)
|
|
||||||
.map(|cmp| (id.to_string(), cmp.update(props)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- state
|
|
||||||
|
|
||||||
/// ### get_value
|
|
||||||
///
|
|
||||||
/// Get component value
|
|
||||||
pub fn get_value(&self, id: &str) -> Option<Payload> {
|
|
||||||
self.components.get(id).map(|cmp| cmp.get_value())
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- events
|
|
||||||
|
|
||||||
/// ### on
|
|
||||||
///
|
|
||||||
/// Handle event for the focused component (if any)
|
|
||||||
/// Returns `None` if no component is focused
|
|
||||||
pub fn on(&mut self, ev: InputEvent) -> Option<(String, Msg)> {
|
|
||||||
match self.focus.as_ref() {
|
|
||||||
None => None,
|
|
||||||
Some(id) => self
|
|
||||||
.components
|
|
||||||
.get_mut(id)
|
|
||||||
.map(|cmp| (id.to_string(), cmp.on(ev))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- focus
|
|
||||||
|
|
||||||
/// ### blur
|
|
||||||
///
|
|
||||||
/// Blur selected element AND DON'T PUSH CURRENT ACTIVE ELEMENT INTO THE STACK
|
|
||||||
/// Last element in stack becomes active and is removed from the stack
|
|
||||||
pub fn blur(&mut self) {
|
|
||||||
if let Some(component) = self.focus.take() {
|
|
||||||
// Blur component
|
|
||||||
if let Some(cmp) = self.components.get_mut(component.as_str()) {
|
|
||||||
cmp.blur();
|
|
||||||
}
|
|
||||||
// Set last element in the stack as active
|
|
||||||
let mut new: Option<String> = None;
|
|
||||||
if let Some(last) = self.focus_stack.last() {
|
|
||||||
// Set focus to last element
|
|
||||||
new = Some(last.clone());
|
|
||||||
self.focus = Some(last.clone());
|
|
||||||
// Active
|
|
||||||
if let Some(new) = self.components.get_mut(last) {
|
|
||||||
new.active();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Pop element from stack
|
|
||||||
if let Some(new) = new {
|
|
||||||
self.pop_from_stack(new.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### active
|
|
||||||
///
|
|
||||||
/// Active provided element
|
|
||||||
/// Current active component, if any, GETS PUSHED to the STACK
|
|
||||||
pub fn active(&mut self, component: &str) {
|
|
||||||
// Active component if exists
|
|
||||||
if let Some(cmp) = self.components.get_mut(component) {
|
|
||||||
// Active component
|
|
||||||
cmp.active();
|
|
||||||
// Put current focus if any, into the stack
|
|
||||||
if let Some(active_component) = self.focus.take() {
|
|
||||||
if active_component != component {
|
|
||||||
// Blur active component if are different
|
|
||||||
if let Some(active_component) =
|
|
||||||
self.components.get_mut(active_component.as_str())
|
|
||||||
{
|
|
||||||
active_component.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.push_to_stack(active_component.as_str());
|
|
||||||
}
|
|
||||||
// Give focus to component
|
|
||||||
self.focus = Some(component.to_string());
|
|
||||||
// Remove new component from the stack
|
|
||||||
self.pop_from_stack(component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- private
|
|
||||||
|
|
||||||
/// ### push_to_stack
|
|
||||||
///
|
|
||||||
/// Push component to stack; first remove it from the stack if any
|
|
||||||
fn push_to_stack(&mut self, name: &str) {
|
|
||||||
self.pop_from_stack(name);
|
|
||||||
self.focus_stack.push(name.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### pop_from_stack
|
|
||||||
///
|
|
||||||
/// Pop element from focus stack
|
|
||||||
fn pop_from_stack(&mut self, name: &str) {
|
|
||||||
self.focus_stack.retain(|c| c.as_str() != name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use super::super::components::input::Input;
|
|
||||||
use super::super::components::text::Text;
|
|
||||||
use super::super::props::{PropValue, TextParts, TextSpan};
|
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_view_init() {
|
|
||||||
let view: View = View::init();
|
|
||||||
// Verify view
|
|
||||||
assert_eq!(view.components.len(), 0);
|
|
||||||
assert!(view.focus.is_none());
|
|
||||||
assert_eq!(view.focus_stack.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_view_mount_umount() {
|
|
||||||
let mut view: View = View::init();
|
|
||||||
// Mount component
|
|
||||||
let input: &str = "INPUT";
|
|
||||||
view.mount(input, make_component_input());
|
|
||||||
// Verify is mounted
|
|
||||||
assert!(view.components.get(input).is_some());
|
|
||||||
// Mount another
|
|
||||||
let text: &str = "TEXT";
|
|
||||||
view.mount(text, make_component_text());
|
|
||||||
assert!(view.components.get(text).is_some());
|
|
||||||
assert_eq!(view.components.len(), 2);
|
|
||||||
// Verify you cannot have duplicates
|
|
||||||
view.mount(input, make_component_input());
|
|
||||||
assert_eq!(view.components.len(), 2); // length should still be 2
|
|
||||||
// Umount
|
|
||||||
view.umount(text);
|
|
||||||
assert_eq!(view.components.len(), 1);
|
|
||||||
assert!(view.components.get(text).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_view_mount_render() {
|
|
||||||
let mut view: View = View::init();
|
|
||||||
// Mount component
|
|
||||||
let input: &str = "INPUT";
|
|
||||||
view.mount(input, make_component_input());
|
|
||||||
assert!(view.render(input).is_some());
|
|
||||||
assert!(view.render("unexisting").is_none());
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_view_focus() {
|
|
||||||
let mut view: View = View::init();
|
|
||||||
// Prepare ids
|
|
||||||
let input1: &str = "INPUT_1";
|
|
||||||
let input2: &str = "INPUT_2";
|
|
||||||
let input3: &str = "INPUT_3";
|
|
||||||
let text1: &str = "TEXT_1";
|
|
||||||
let text2: &str = "TEXT_2";
|
|
||||||
// Mount components
|
|
||||||
view.mount(input1, make_component_input());
|
|
||||||
view.mount(input2, make_component_input());
|
|
||||||
view.mount(input3, make_component_input());
|
|
||||||
view.mount(text1, make_component_text());
|
|
||||||
view.mount(text2, make_component_text());
|
|
||||||
// Verify focus
|
|
||||||
assert!(view.focus.is_none());
|
|
||||||
assert_eq!(view.focus_stack.len(), 0);
|
|
||||||
// Blur when nothing is selected
|
|
||||||
view.blur();
|
|
||||||
assert!(view.focus.is_none());
|
|
||||||
assert_eq!(view.focus_stack.len(), 0);
|
|
||||||
// Active unexisting component
|
|
||||||
view.active("UNEXISTING-COMPONENT");
|
|
||||||
assert!(view.focus.is_none());
|
|
||||||
assert_eq!(view.focus_stack.len(), 0);
|
|
||||||
// Give focus to a component
|
|
||||||
view.active(input1);
|
|
||||||
// Check focus
|
|
||||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input1);
|
|
||||||
assert_eq!(view.focus_stack.len(), 0); // NOTE: stack is empty until a focus gets blurred
|
|
||||||
// Active a new component
|
|
||||||
view.active(input2);
|
|
||||||
// Now focus should be on input2, but input 1 should be in the focus stack
|
|
||||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input2);
|
|
||||||
assert_eq!(view.focus_stack.len(), 1);
|
|
||||||
assert_eq!(view.focus_stack[0].as_str(), input1);
|
|
||||||
// Active input 3
|
|
||||||
view.active(input3);
|
|
||||||
// now focus should be hold by input3, and stack should have len 2
|
|
||||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input3);
|
|
||||||
assert_eq!(view.focus_stack.len(), 2);
|
|
||||||
assert_eq!(view.focus_stack[0].as_str(), input1);
|
|
||||||
assert_eq!(view.focus_stack[1].as_str(), input2);
|
|
||||||
// blur
|
|
||||||
view.blur();
|
|
||||||
// Focus should now be hold by input2; input 3 should NOT be in the stack
|
|
||||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input2);
|
|
||||||
assert_eq!(view.focus_stack.len(), 1);
|
|
||||||
assert_eq!(view.focus_stack[0].as_str(), input1);
|
|
||||||
// Active twice
|
|
||||||
view.active(input2);
|
|
||||||
// Nothing should have changed
|
|
||||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input2);
|
|
||||||
assert_eq!(view.focus_stack.len(), 1);
|
|
||||||
assert_eq!(view.focus_stack[0].as_str(), input1);
|
|
||||||
// Blur again; stack should become empty, whether focus should then be hold by input 1
|
|
||||||
view.blur();
|
|
||||||
assert_eq!(view.focus.as_ref().unwrap().as_str(), input1);
|
|
||||||
assert_eq!(view.focus_stack.len(), 0);
|
|
||||||
// Blur again; now everything should be none
|
|
||||||
view.blur();
|
|
||||||
assert!(view.focus.is_none());
|
|
||||||
assert_eq!(view.focus_stack.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_view_focus_umount() {
|
|
||||||
let mut view: View = View::init();
|
|
||||||
// Mount component
|
|
||||||
let input: &str = "INPUT";
|
|
||||||
let text: &str = "TEXT";
|
|
||||||
let text2: &str = "TEXT2";
|
|
||||||
view.mount(input, make_component_input());
|
|
||||||
view.mount(text, make_component_text());
|
|
||||||
view.mount(text2, make_component_text());
|
|
||||||
// Give focus to input
|
|
||||||
view.active(input);
|
|
||||||
// Give focus to text
|
|
||||||
view.active(text);
|
|
||||||
view.active(text2);
|
|
||||||
// Stack should have 1 element
|
|
||||||
assert_eq!(view.focus_stack.len(), 2);
|
|
||||||
// Focus should be some
|
|
||||||
assert!(view.focus.is_some());
|
|
||||||
// Umount text
|
|
||||||
view.umount(text2);
|
|
||||||
// Focus should now be hold by 'text'; stack should now have size 1
|
|
||||||
assert_eq!(view.focus.as_ref().unwrap(), text);
|
|
||||||
assert_eq!(view.focus_stack.len(), 1);
|
|
||||||
// Umount input
|
|
||||||
view.umount(input);
|
|
||||||
assert_eq!(view.focus.as_ref().unwrap(), text);
|
|
||||||
assert_eq!(view.focus_stack.len(), 0);
|
|
||||||
// Umount text
|
|
||||||
view.umount(text);
|
|
||||||
assert!(view.focus.is_none());
|
|
||||||
assert_eq!(view.focus_stack.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_view_update() {
|
|
||||||
let mut view: View = View::init();
|
|
||||||
// Prepare ids
|
|
||||||
let text: &str = "TEXT";
|
|
||||||
// Mount text
|
|
||||||
view.mount(text, make_component_text());
|
|
||||||
// Get properties and update
|
|
||||||
let props: Props = view.get_props(text).unwrap().build();
|
|
||||||
// Verify bold is false
|
|
||||||
assert_eq!(props.bold, false);
|
|
||||||
// Update properties and set bold to true
|
|
||||||
let mut builder = view.get_props(text).unwrap();
|
|
||||||
let (id, msg) = view.update(text, builder.bold().build()).unwrap();
|
|
||||||
// Verify return values
|
|
||||||
assert_eq!(id, text);
|
|
||||||
assert_eq!(msg, Msg::None);
|
|
||||||
// Verify bold is now true
|
|
||||||
let props: Props = view.get_props(text).unwrap().build();
|
|
||||||
// Verify bold is false
|
|
||||||
assert_eq!(props.bold, true);
|
|
||||||
// Get properties for unexisting component
|
|
||||||
assert!(view.update("foobar", props).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ui_layout_view_on() {
|
|
||||||
let mut view: View = View::init();
|
|
||||||
// Prepare ids
|
|
||||||
let text: &str = "TEXT";
|
|
||||||
let input: &str = "INPUT";
|
|
||||||
// Mount
|
|
||||||
view.mount(text, make_component_text());
|
|
||||||
view.mount(input, make_component_input());
|
|
||||||
// Verify current value
|
|
||||||
assert_eq!(view.get_value(text).unwrap(), Payload::None); // Text value is Nothing
|
|
||||||
assert_eq!(
|
|
||||||
view.get_value(input).unwrap(),
|
|
||||||
Payload::Text(String::from("text"))
|
|
||||||
); // Defined in `make_component_input`
|
|
||||||
// Handle events WITHOUT ANY ACTIVE ELEMENT
|
|
||||||
assert!(view
|
|
||||||
.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter)))
|
|
||||||
.is_none());
|
|
||||||
// Active input
|
|
||||||
view.active(input);
|
|
||||||
// Now handle events on input
|
|
||||||
// Try char
|
|
||||||
assert_eq!(
|
|
||||||
view.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('1'))))
|
|
||||||
.unwrap(),
|
|
||||||
(input.to_string(), Msg::None)
|
|
||||||
);
|
|
||||||
// Verify new value
|
|
||||||
assert_eq!(
|
|
||||||
view.get_value(input).unwrap(),
|
|
||||||
Payload::Text(String::from("text1"))
|
|
||||||
);
|
|
||||||
// Verify enter
|
|
||||||
assert_eq!(
|
|
||||||
view.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter)))
|
|
||||||
.unwrap(),
|
|
||||||
(
|
|
||||||
input.to_string(),
|
|
||||||
Msg::OnSubmit(Payload::Text(String::from("text1")))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### make_component
|
|
||||||
///
|
|
||||||
/// Make a new component; we'll use Input, which uses a rich set of events
|
|
||||||
fn make_component_input() -> Box<dyn Component> {
|
|
||||||
Box::new(Input::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_texts(TextParts::new(Some(String::from("mytext")), None))
|
|
||||||
.with_value(PropValue::Str(String::from("text")))
|
|
||||||
.build(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_component_text() -> Box<dyn Component> {
|
|
||||||
Box::new(Text::new(
|
|
||||||
PropsBuilder::default()
|
|
||||||
.with_texts(TextParts::new(
|
|
||||||
None,
|
|
||||||
Some(vec![TextSpan::from("Sample text")]),
|
|
||||||
))
|
|
||||||
.build(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,8 @@
|
|||||||
*/
|
*/
|
||||||
// Modules
|
// Modules
|
||||||
pub mod activities;
|
pub mod activities;
|
||||||
|
pub(crate) mod components;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub(crate) mod input;
|
pub(crate) mod input;
|
||||||
pub(crate) mod layout;
|
pub(crate) mod keymap;
|
||||||
pub(crate) mod store;
|
pub(crate) mod store;
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ui_store() {
|
fn test_ui_store() {
|
||||||
// Create store
|
// Create store
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_utils_crypto_aes128() {
|
fn test_utils_crypto_aes128() {
|
||||||
let key: &str = "MYSUPERSECRETKEY";
|
let key: &str = "MYSUPERSECRETKEY";
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
//! ## File
|
||||||
|
//!
|
||||||
|
//! `file` is the module which exposes file related utilities
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
use std::fs::File;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// ### open_file
|
||||||
|
///
|
||||||
|
/// Open file provided as parameter
|
||||||
|
pub fn open_file<P>(filename: P, create: bool, write: bool, append: bool) -> io::Result<File>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
OpenOptions::new()
|
||||||
|
.create(create)
|
||||||
|
.write(write)
|
||||||
|
.append(append)
|
||||||
|
.truncate(!append)
|
||||||
|
.open(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_utils_file_open() {
|
||||||
|
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
assert!(open_file(tmpfile.path(), true, true, true).is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -152,11 +152,20 @@ pub fn fmt_path_elide(p: &Path, width: usize) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### shadow_password
|
||||||
|
///
|
||||||
|
/// Return a string with the same length of input string, but each character is replaced by '*'
|
||||||
|
pub fn shadow_password(s: &str) -> String {
|
||||||
|
(0..s.len()).map(|_| '*').collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_utils_fmt_pex() {
|
fn test_utils_fmt_pex() {
|
||||||
assert_eq!(fmt_pex(7, 7, 7), String::from("rwxrwxrwx"));
|
assert_eq!(fmt_pex(7, 7, 7), String::from("rwxrwxrwx"));
|
||||||
@@ -217,4 +226,9 @@ mod tests {
|
|||||||
let p: &Path = &Path::new("/develop/pippo/foo/bar");
|
let p: &Path = &Path::new("/develop/pippo/foo/bar");
|
||||||
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/.../foo/bar"));
|
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/.../foo/bar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_utils_fmt_shadow_password() {
|
||||||
|
assert_eq!(shadow_password("foobar"), String::from("******"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
*/
|
*/
|
||||||
// modules
|
// modules
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
|
pub mod file;
|
||||||
pub mod fmt;
|
pub mod fmt;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod random;
|
pub mod random;
|
||||||
|
pub mod ui;
|
||||||
|
|||||||
@@ -233,6 +233,8 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::utils::fmt::fmt_time;
|
use crate::utils::fmt::fmt_time;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_utils_parse_remote_opt() {
|
fn test_utils_parse_remote_opt() {
|
||||||
// Base case
|
// Base case
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_utils_random_alphanumeric_with_len() {
|
fn test_utils_random_alphanumeric_with_len() {
|
||||||
assert_eq!(random_alphanumeric_with_len(256).len(), 256);
|
assert_eq!(random_alphanumeric_with_len(256).len(), 256);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
|
|
||||||
/// ### draw_area_in
|
/// ### draw_area_in
|
||||||
///
|
///
|
||||||
@@ -60,8 +60,10 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ui_layout_utils_draw_area_in() {
|
fn test_utils_ui_draw_area_in() {
|
||||||
let area: Rect = Rect::new(0, 0, 1024, 512);
|
let area: Rect = Rect::new(0, 0, 1024, 512);
|
||||||
let child: Rect = draw_area_in(area, 75, 30);
|
let child: Rect = draw_area_in(area, 75, 30);
|
||||||
assert_eq!(child.x, 43);
|
assert_eq!(child.x, 43);
|
||||||