Notifications
This commit is contained in:
parent
f0a91b1579
commit
198d421ab0
1
.github/actions-rs/grcov.yml
vendored
1
.github/actions-rs/grcov.yml
vendored
|
@ -11,6 +11,7 @@ ignore:
|
||||||
- src/activity_manager.rs
|
- src/activity_manager.rs
|
||||||
- src/filetransfer/transfer/s3/mod.rs
|
- src/filetransfer/transfer/s3/mod.rs
|
||||||
- src/support.rs
|
- src/support.rs
|
||||||
|
- src/system/notifications.rs
|
||||||
- "src/ui/activities/*"
|
- "src/ui/activities/*"
|
||||||
- src/ui/context.rs
|
- src/ui/context.rs
|
||||||
- src/ui/input.rs
|
- src/ui/input.rs
|
||||||
|
|
2
.github/workflows/coverage.yml
vendored
2
.github/workflows/coverage.yml
vendored
|
@ -11,6 +11,8 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev
|
||||||
- name: Setup containers
|
- name: Setup containers
|
||||||
run: docker-compose -f "tests/docker-compose.yml" up -d --build
|
run: docker-compose -f "tests/docker-compose.yml" up -d --build
|
||||||
- name: Setup nightly toolchain
|
- name: Setup nightly toolchain
|
||||||
|
|
2
.github/workflows/freebsd.yml
vendored
2
.github/workflows/freebsd.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
||||||
uses: vmactions/freebsd-vm@v0.1.4
|
uses: vmactions/freebsd-vm@v0.1.4
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
prepare: pkg install -y curl wget libssh gcc vim
|
prepare: pkg install -y curl wget libssh gcc vim dbus pkgconf
|
||||||
run: |
|
run: |
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rustup.sh && \
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rustup.sh && \
|
||||||
chmod +x /tmp/rustup.sh && \
|
chmod +x /tmp/rustup.sh && \
|
||||||
|
|
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
|
@ -11,6 +11,8 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev
|
||||||
- name: Setup containers
|
- name: Setup containers
|
||||||
run: docker-compose -f "tests/docker-compose.yml" up -d --build
|
run: docker-compose -f "tests/docker-compose.yml" up -d --build
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -27,7 +27,7 @@ Released on ??
|
||||||
|
|
||||||
> 🍁 Autumn update 🍇
|
> 🍁 Autumn update 🍇
|
||||||
|
|
||||||
- **Aws S3**
|
- **Aws S3** 🪣
|
||||||
- Added support for the aws-s3 protocol.
|
- Added support for the aws-s3 protocol.
|
||||||
- Operate on your bucket directly from the file explorer.
|
- Operate on your bucket directly from the file explorer.
|
||||||
- You can also save your buckets as bookmarks.
|
- You can also save your buckets as bookmarks.
|
||||||
|
@ -36,7 +36,14 @@ Released on ??
|
||||||
- Possibility to update termscp directly via GUI or CLI.
|
- Possibility to update termscp directly via GUI or CLI.
|
||||||
- Install update via CLI running `(sudo) termscp --update`.
|
- Install update via CLI running `(sudo) termscp --update`.
|
||||||
- Install update via GUI from auth form: when the "new version message" is displayed press `<CTRL+R>`, then enter `YES` in the radio input asking whether to install the update.
|
- Install update via GUI from auth form: when the "new version message" is displayed press `<CTRL+R>`, then enter `YES` in the radio input asking whether to install the update.
|
||||||
- **Prompt user when about to replace existing file on a file transfer**:
|
- **Notifications** 📫
|
||||||
|
- termscp will now send Desktop notifications in these cases
|
||||||
|
- on transfer completed (minimum transfer size can be specified in configuration; default 512MB)
|
||||||
|
- on transfer error (same as above)
|
||||||
|
- on update available
|
||||||
|
- Added "notifications enabled" in configuration (Default enabled)
|
||||||
|
- Added "Notifications: minimum transfer size": if transfer size is greater or equal than the specified value, notifications for transfer will be displayed.
|
||||||
|
- **Prompt user when about to replace existing file on a file transfer** ❓
|
||||||
- Whenever a file transfer is about to replace an existing file on local/remote host, you will be prompted if you're sure you really want to replace that file.
|
- Whenever a file transfer is about to replace an existing file on local/remote host, you will be prompted if you're sure you really want to replace that file.
|
||||||
- You may want to disable this option. You can go to configuration and set "Prompt when replacing existing files?" to "NO"
|
- You may want to disable this option. You can go to configuration and set "Prompt when replacing existing files?" to "NO"
|
||||||
- **❗ BREAKING CHANGES ❗**:
|
- **❗ BREAKING CHANGES ❗**:
|
||||||
|
@ -45,6 +52,7 @@ Released on ??
|
||||||
- Reuse mounts in UI, in order to reduce executable size
|
- Reuse mounts in UI, in order to reduce executable size
|
||||||
- Dependencies:
|
- Dependencies:
|
||||||
- Added `rust-s3 0.27-rc4`
|
- Added `rust-s3 0.27-rc4`
|
||||||
|
- Added `notify_rust 4.5.3`
|
||||||
- Added `self_update 0.27.0`
|
- Added `self_update 0.27.0`
|
||||||
- Updated `argh` to `0.1.6`
|
- Updated `argh` to `0.1.6`
|
||||||
- Updated `dirs` to `4.0.0`
|
- Updated `dirs` to `4.0.0`
|
||||||
|
|
310
Cargo.lock
generated
310
Cargo.lock
generated
|
@ -88,8 +88,8 @@ dependencies = [
|
||||||
"argh_shared",
|
"argh_shared",
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote 1.0.9",
|
||||||
"syn",
|
"syn 1.0.76",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -98,6 +98,18 @@ version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38de00daab4eac7d753e97697066238d67ce9d7e2d823ab4f72fe14af29f3f33"
|
checksum = "38de00daab4eac7d753e97697066238d67ce9d7e2d823ab4f72fe14af29f3f33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayref"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.51"
|
version = "0.1.51"
|
||||||
|
@ -105,8 +117,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
|
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote 1.0.9",
|
||||||
"syn",
|
"syn 1.0.76",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -176,12 +188,35 @@ version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[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]]
|
||||||
|
name = "block"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -284,7 +319,7 @@ version = "0.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -302,6 +337,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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"
|
||||||
|
@ -370,13 +411,23 @@ dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d"
|
checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
|
@ -411,8 +462,8 @@ version = "0.1.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa"
|
checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote 1.0.9",
|
||||||
"syn",
|
"syn 1.0.76",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -424,6 +475,17 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dbus"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60b225f88dd3718253526c38bc333b9986b547a7580abf81186c9461647b2487"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"libdbus-sys",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "debug-helper"
|
name = "debug-helper"
|
||||||
version = "0.3.12"
|
version = "0.3.12"
|
||||||
|
@ -456,6 +518,17 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"redox_users 0.3.5",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "3.0.2"
|
version = "3.0.2"
|
||||||
|
@ -481,7 +554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
|
checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"redox_users",
|
"redox_users 0.4.0",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -908,6 +981,15 @@ version = "0.2.102"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
|
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libdbus-sys"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b"
|
||||||
|
dependencies = [
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libssh2-sys"
|
name = "libssh2-sys"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
|
@ -961,6 +1043,18 @@ dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac-notification-sys"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3dfb6b71a9a89cd38b395d994214297447e8e63b1ba5708a9a2b0b1048ceda76"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"chrono",
|
||||||
|
"dirs 1.0.5",
|
||||||
|
"objc-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "magic-crypt"
|
name = "magic-crypt"
|
||||||
version = "3.1.8"
|
version = "3.1.8"
|
||||||
|
@ -978,6 +1072,15 @@ dependencies = [
|
||||||
"tiger",
|
"tiger",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "malloc_buf"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "match_cfg"
|
name = "match_cfg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -997,8 +1100,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6007f9dad048e0a224f27ca599d669fca8cfa0dac804725aab542b2eb032bce6"
|
checksum = "6007f9dad048e0a224f27ca599d669fca8cfa0dac804725aab542b2eb032bce6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote 1.0.9",
|
||||||
"syn",
|
"syn 1.0.76",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1089,6 +1192,17 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-rust"
|
||||||
|
version = "4.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4b2d5d72d16b6abdb6fa2c364d9363e23d6ed7c20c1a1e85fd8cd880144442c"
|
||||||
|
dependencies = [
|
||||||
|
"dbus 0.9.4",
|
||||||
|
"mac-notification-sys",
|
||||||
|
"winrt-notification",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ntapi"
|
name = "ntapi"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
@ -1190,6 +1304,35 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
|
checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||||
|
dependencies = [
|
||||||
|
"malloc_buf",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc-foundation"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||||
|
dependencies = [
|
||||||
|
"block",
|
||||||
|
"objc",
|
||||||
|
"objc_id",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc_id"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
|
||||||
|
dependencies = [
|
||||||
|
"objc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
@ -1218,7 +1361,7 @@ version = "0.10.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a"
|
checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1382,7 +1525,7 @@ version = "1.0.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1394,6 +1537,12 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "0.3.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
|
@ -1496,7 +1645,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 = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
|
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.1.16",
|
||||||
|
"redox_syscall 0.1.57",
|
||||||
|
"rust-argon2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1580,6 +1740,18 @@ 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 = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
|
@ -1650,7 +1822,7 @@ checksum = "6d752040301c251d653aa740dec847e95767ce312cfc469bee85eb13cbf81d8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"block-modes",
|
"block-modes",
|
||||||
"dbus",
|
"dbus 0.2.3",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"num",
|
"num",
|
||||||
|
@ -1664,7 +1836,7 @@ 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 = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
|
checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"core-foundation 0.7.0",
|
"core-foundation 0.7.0",
|
||||||
"core-foundation-sys 0.7.0",
|
"core-foundation-sys 0.7.0",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1677,7 +1849,7 @@ version = "2.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
|
checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"core-foundation 0.9.1",
|
"core-foundation 0.9.1",
|
||||||
"core-foundation-sys 0.8.2",
|
"core-foundation-sys 0.8.2",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1761,7 +1933,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"xml-rs",
|
"xml-rs 0.8.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1771,8 +1943,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
|
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote 1.0.9",
|
||||||
"syn",
|
"syn 1.0.76",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1886,12 +2058,28 @@ version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d876d4d57f6bbf2245d43f7ec53759461f801a446d3693704aa6d27b257844d7"
|
checksum = "d876d4d57f6bbf2245d43f7ec53759461f801a446d3693704aa6d27b257844d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"libc",
|
"libc",
|
||||||
"libssh2-sys",
|
"libssh2-sys",
|
||||||
"parking_lot 0.10.2",
|
"parking_lot 0.10.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ca6e4730f517e041e547ffe23d29daab8de6b73af4b6ae2a002108169f5e7da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3384590878eb0cab3b128e844412e2d010821e7e091211b9d87324173ada7db8"
|
||||||
|
dependencies = [
|
||||||
|
"quote 0.3.15",
|
||||||
|
"syn 0.11.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
|
@ -1911,6 +2099,17 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "0.11.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
|
||||||
|
dependencies = [
|
||||||
|
"quote 0.3.15",
|
||||||
|
"synom",
|
||||||
|
"unicode-xid 0.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.76"
|
version = "1.0.76"
|
||||||
|
@ -1918,8 +2117,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
|
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote 1.0.9",
|
||||||
"unicode-xid",
|
"unicode-xid 0.2.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "synom"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-xid 0.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1971,7 +2179,7 @@ name = "termscp"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argh",
|
"argh",
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"bytesize",
|
"bytesize",
|
||||||
"chrono",
|
"chrono",
|
||||||
"content_inspector",
|
"content_inspector",
|
||||||
|
@ -1983,6 +2191,7 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"magic-crypt",
|
"magic-crypt",
|
||||||
|
"notify-rust",
|
||||||
"open",
|
"open",
|
||||||
"path-slash",
|
"path-slash",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
|
@ -2033,8 +2242,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
|
checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote 1.0.9",
|
||||||
"syn",
|
"syn 1.0.76",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2172,7 +2381,7 @@ version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23"
|
checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"cassowary",
|
"cassowary",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
@ -2248,6 +2457,12 @@ version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -2332,8 +2547,8 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote 1.0.9",
|
||||||
"syn",
|
"syn 1.0.76",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2355,7 +2570,7 @@ version = "0.2.78"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
|
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote 1.0.9",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2366,8 +2581,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
|
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote 1.0.9",
|
||||||
"syn",
|
"syn 1.0.76",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
@ -2461,6 +2676,28 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winrt"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e30cba82e22b083dc5a422c2ee77e20dc7927271a0dc981360c57c1453cb48d"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winrt-notification"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57790eb281688a4682dab44df2a1ba8b78373233bd71cb291c3e75fecb1a01c4"
|
||||||
|
dependencies = [
|
||||||
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
|
"winapi",
|
||||||
|
"winrt",
|
||||||
|
"xml-rs 0.6.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xattr"
|
name = "xattr"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -2470,6 +2707,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xml-rs"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1945e12e16b951721d7976520b0832496ef79c31602c7a29d950de79ba74621"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 0.9.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xml-rs"
|
name = "xml-rs"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
|
|
@ -40,6 +40,7 @@ keyring = { version = "0.10.1", optional = true }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
magic-crypt = "3.1.7"
|
magic-crypt = "3.1.7"
|
||||||
|
notify-rust = { version = "4.5.3", default-features = false, features = [ "d" ] }
|
||||||
open = "2.0.1"
|
open = "2.0.1"
|
||||||
rand = "0.8.4"
|
rand = "0.8.4"
|
||||||
regex = "1.5.4"
|
regex = "1.5.4"
|
||||||
|
|
|
@ -82,8 +82,11 @@ For more information or other platforms, please visit [veeso.github.io](https://
|
||||||
- **Linux** users:
|
- **Linux** users:
|
||||||
- libssh
|
- libssh
|
||||||
- libdbus-1
|
- libdbus-1
|
||||||
|
- pkg-config
|
||||||
- **BSD** users:
|
- **BSD** users:
|
||||||
- libssh
|
- libssh
|
||||||
|
- dbus
|
||||||
|
- pkgconf
|
||||||
|
|
||||||
### Optional Requirements ✔️
|
### Optional Requirements ✔️
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
- [Text Editor ✏](#text-editor-)
|
- [Text Editor ✏](#text-editor-)
|
||||||
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
|
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
|
||||||
- [Logging 🩺](#logging-)
|
- [Logging 🩺](#logging-)
|
||||||
|
- [Notifications 📫](#notifications-)
|
||||||
|
|
||||||
## Usage ❓
|
## Usage ❓
|
||||||
|
|
||||||
|
@ -304,6 +305,8 @@ These parameters can be changed:
|
||||||
- **Group Dirs**: select whether directories should be groupped or not in file explorers. If `Display first` is selected, directories will be sorted using the configured method but displayed before files, viceversa if `Display last` is selected.
|
- **Group Dirs**: select whether directories should be groupped or not in file explorers. If `Display first` is selected, directories will be sorted using the configured method but displayed before files, viceversa if `Display last` is selected.
|
||||||
- **Remote File formatter syntax**: syntax to display file info for each file in the remote explorer. See [File explorer format](#file-explorer-format)
|
- **Remote File formatter syntax**: syntax to display file info for each file in the remote explorer. See [File explorer format](#file-explorer-format)
|
||||||
- **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format)
|
- **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format)
|
||||||
|
- **Enable notifications?**: If set to `Yes`, notifications will be displayed.
|
||||||
|
- **Notifications: minimum transfer size**: if transfer size is greater or equal than the specified value, notifications for transfer will be displayed. The accepted values are in format `{UNSIGNED} B/KB/MB/GB/TB/PB`
|
||||||
|
|
||||||
### SSH Key Storage 🔐
|
### SSH Key Storage 🔐
|
||||||
|
|
||||||
|
@ -471,3 +474,18 @@ Yes, you can. Just start termscp with `-q or --quiet` option. You can alias term
|
||||||
> Is logging safe?
|
> 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.
|
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.
|
||||||
|
|
||||||
|
## Notifications 📫
|
||||||
|
|
||||||
|
Termscp will send Desktop notifications for these kind of events:
|
||||||
|
|
||||||
|
- on **Transfer completed**: The notification will be sent once a transfer has been successfully completed.
|
||||||
|
- ❗ The notification will be displayed only if the transfer total size is at least the specified `Notifications: minimum transfer size` in the configuration.
|
||||||
|
- on **Transfer failed**: The notification will be sent once a transfer has failed due to an error.
|
||||||
|
- ❗ The notification will be displayed only if the transfer total size is at least the specified `Notifications: minimum transfer size` in the configuration.
|
||||||
|
- on **Update available**: Whenever a new version of termscp is available, a notification will be displayed.
|
||||||
|
- on **Update installed**: Whenever a new version of termscp has been installed, a notification will be displayed.
|
||||||
|
- on **Update failed**: Whenever the installation of the update fails, a notification will be displayed.
|
||||||
|
|
||||||
|
❗ If you prefer to keep notifications turned off, you can just enter setup and set `Enable notifications?` to `No` 😉.
|
||||||
|
❗ If you want to change the minimum transfer size to display notifications, you can change the value in the configuration with key `Notifications: minimum transfer size` and set it to whatever suits better for you 🙂.
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
- [文本编辑器](#文本编辑器)
|
- [文本编辑器](#文本编辑器)
|
||||||
- [如何配置文本编辑器?](#如何配置文本编辑器)
|
- [如何配置文本编辑器?](#如何配置文本编辑器)
|
||||||
- [日志](#日志)
|
- [日志](#日志)
|
||||||
|
- [通知](#通知)
|
||||||
|
|
||||||
## 用法
|
## 用法
|
||||||
|
|
||||||
|
@ -297,6 +298,8 @@ termscp和书签一样,只需要保证这些路径是可访问的:
|
||||||
- **Group Dirs**:选择在文件浏览器中是否对文件夹进行分组。如果选择 `Display first`,目录将根据设置的方法排序,但仍显示在文件之前;如果选择 `Display last`,则正好相反。
|
- **Group Dirs**:选择在文件浏览器中是否对文件夹进行分组。如果选择 `Display first`,目录将根据设置的方法排序,但仍显示在文件之前;如果选择 `Display last`,则正好相反。
|
||||||
- **Remote File formatter syntax**:在远程资源管理器中为每个文件显示文件信息的语法。参见[资源管理器格式](#资源管理器格式)
|
- **Remote File formatter syntax**:在远程资源管理器中为每个文件显示文件信息的语法。参见[资源管理器格式](#资源管理器格式)
|
||||||
- **Local File formatter syntax**:在本地资源管理器中显示每个文件的文件信息的语法。参见[资源管理器格式](#资源管理器格式)
|
- **Local File formatter syntax**:在本地资源管理器中显示每个文件的文件信息的语法。参见[资源管理器格式](#资源管理器格式)
|
||||||
|
- **Enable notifications?**: 如果设置为 `Yes`,则会显示通知。
|
||||||
|
- **Notifications: minimum transfer size**: 如果传输大小大于或等于指定值,将显示传输通知。 接受的值格式为 `{UNSIGNED} B/KB/MB/GB/TB/PB`
|
||||||
|
|
||||||
### SSH Key Storage
|
### SSH Key Storage
|
||||||
|
|
||||||
|
@ -462,3 +465,18 @@ termscp会为每个会话创建一个日志文件,该文件在
|
||||||
> 日志是安全的吗?
|
> 日志是安全的吗?
|
||||||
|
|
||||||
如果你担心安全问题,日志文件不包含任何普通的密码,所以不用担心,它暴露的信息与同级文件 `书签` 报告的信息相同。
|
如果你担心安全问题,日志文件不包含任何普通的密码,所以不用担心,它暴露的信息与同级文件 `书签` 报告的信息相同。
|
||||||
|
|
||||||
|
## 通知
|
||||||
|
|
||||||
|
termscp 将针对这些类型的事件发送桌面通知:
|
||||||
|
|
||||||
|
- **传输完成**: 传输成功完成后将发送通知。
|
||||||
|
- ❗ 仅当传输总大小至少为配置中指定的 `Notifications: minimum transfer size` 时才会显示通知。
|
||||||
|
- **传输失败**:一旦传输因错误而失败,将发送通知。
|
||||||
|
- ❗ 仅当传输总大小至少为配置中指定的 `Notifications: minimum transfer size` 时才会显示通知。
|
||||||
|
- **更新可用**:每当有新版本的termscp 可用时,都会显示通知。
|
||||||
|
- **更新已安装**:每当安装了新版本的termscp 时,都会显示通知。
|
||||||
|
- **更新失败**:每当更新安装失败时,都会显示通知。
|
||||||
|
|
||||||
|
❗ 如果您希望保持关闭通知,您只需进入设置并将 `Enable notifications?` 设置为 `No`😉。
|
||||||
|
❗ 如果您想更改最小传输大小以显示通知,您可以使用键 `Notifications: minimum transfer size` 更改配置中的值,并将其设置为更适合您的任何值🙂。
|
||||||
|
|
|
@ -278,7 +278,7 @@ install_bsd_cargo_deps() {
|
||||||
set -e
|
set -e
|
||||||
confirm "${YELLOW}libssh, gcc${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?"
|
confirm "${YELLOW}libssh, gcc${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?"
|
||||||
sudo="$(elevate_priv_ex /usr/local/bin)"
|
sudo="$(elevate_priv_ex /usr/local/bin)"
|
||||||
$sudo pkg install -y curl wget libssh gcc
|
$sudo pkg install -y curl wget libssh gcc dbus pkgconf
|
||||||
info "Dependencies installed successfully"
|
info "Dependencies installed successfully"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,8 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub const DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD: u64 = 536870912; // 512MB
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||||
/// ## UserConfig
|
/// ## UserConfig
|
||||||
///
|
///
|
||||||
|
@ -56,6 +58,8 @@ pub struct UserInterfaceConfig {
|
||||||
pub group_dirs: Option<String>,
|
pub group_dirs: Option<String>,
|
||||||
pub file_fmt: Option<String>, // Refers to local host (for backward compatibility)
|
pub file_fmt: Option<String>, // Refers to local host (for backward compatibility)
|
||||||
pub remote_file_fmt: Option<String>, // @! Since 0.5.0
|
pub remote_file_fmt: Option<String>, // @! Since 0.5.0
|
||||||
|
pub notifications: Option<bool>, // @! Since 0.7.0; Default true
|
||||||
|
pub notification_threshold: Option<u64>, // @! Since 0.7.0; Default 512MB
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||||
|
@ -89,6 +93,8 @@ impl Default for UserInterfaceConfig {
|
||||||
group_dirs: None,
|
group_dirs: None,
|
||||||
file_fmt: None,
|
file_fmt: None,
|
||||||
remote_file_fmt: None,
|
remote_file_fmt: None,
|
||||||
|
notifications: Some(true),
|
||||||
|
notification_threshold: Some(DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,6 +132,8 @@ mod tests {
|
||||||
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}")),
|
remote_file_fmt: Some(String::from("{USER}")),
|
||||||
|
notifications: Some(true),
|
||||||
|
notification_threshold: Some(DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD),
|
||||||
};
|
};
|
||||||
assert_eq!(ui.default_protocol, String::from("SFTP"));
|
assert_eq!(ui.default_protocol, String::from("SFTP"));
|
||||||
assert_eq!(ui.text_editor, PathBuf::from("nano"));
|
assert_eq!(ui.text_editor, PathBuf::from("nano"));
|
||||||
|
@ -156,5 +164,10 @@ mod tests {
|
||||||
cfg.user_interface.remote_file_fmt,
|
cfg.user_interface.remote_file_fmt,
|
||||||
Some(String::from("{USER}"))
|
Some(String::from("{USER}"))
|
||||||
);
|
);
|
||||||
|
assert_eq!(cfg.user_interface.notifications, Some(true));
|
||||||
|
assert_eq!(
|
||||||
|
cfg.user_interface.notification_threshold,
|
||||||
|
Some(DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -202,6 +202,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.check_for_updates.unwrap(), true);
|
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
|
||||||
assert_eq!(cfg.user_interface.prompt_on_file_replace.unwrap(), false);
|
assert_eq!(cfg.user_interface.prompt_on_file_replace.unwrap(), false);
|
||||||
|
assert_eq!(cfg.user_interface.notifications.unwrap(), false);
|
||||||
|
assert_eq!(cfg.user_interface.notification_threshold.unwrap(), 1024);
|
||||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
|
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cfg.user_interface.file_fmt,
|
cfg.user_interface.file_fmt,
|
||||||
|
@ -248,6 +250,8 @@ mod tests {
|
||||||
assert!(cfg.user_interface.prompt_on_file_replace.is_none());
|
assert!(cfg.user_interface.prompt_on_file_replace.is_none());
|
||||||
assert!(cfg.user_interface.file_fmt.is_none());
|
assert!(cfg.user_interface.file_fmt.is_none());
|
||||||
assert!(cfg.user_interface.remote_file_fmt.is_none());
|
assert!(cfg.user_interface.remote_file_fmt.is_none());
|
||||||
|
assert!(cfg.user_interface.notifications.is_none());
|
||||||
|
assert!(cfg.user_interface.notification_threshold.is_none());
|
||||||
// Verify keys
|
// Verify keys
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*cfg.remote
|
*cfg.remote
|
||||||
|
@ -323,6 +327,8 @@ mod tests {
|
||||||
group_dirs = "last"
|
group_dirs = "last"
|
||||||
file_fmt = "{NAME} {PEX}"
|
file_fmt = "{NAME} {PEX}"
|
||||||
remote_file_fmt = "{NAME} {USER}"
|
remote_file_fmt = "{NAME} {USER}"
|
||||||
|
notifications = false
|
||||||
|
notification_threshold = 1024
|
||||||
|
|
||||||
[remote.ssh_keys]
|
[remote.ssh_keys]
|
||||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||||
|
|
|
@ -47,6 +47,7 @@ extern crate lazy_static;
|
||||||
extern crate log;
|
extern crate log;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate magic_crypt;
|
extern crate magic_crypt;
|
||||||
|
extern crate notify_rust;
|
||||||
extern crate open;
|
extern crate open;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
extern crate path_slash;
|
extern crate path_slash;
|
||||||
|
|
|
@ -28,7 +28,9 @@
|
||||||
// mod
|
// mod
|
||||||
use crate::system::{
|
use crate::system::{
|
||||||
auto_update::{Update, UpdateStatus},
|
auto_update::{Update, UpdateStatus},
|
||||||
|
config_client::ConfigClient,
|
||||||
environment,
|
environment,
|
||||||
|
notifications::Notification,
|
||||||
theme_provider::ThemeProvider,
|
theme_provider::ThemeProvider,
|
||||||
};
|
};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
@ -66,9 +68,23 @@ pub fn install_update() -> Result<String, String> {
|
||||||
{
|
{
|
||||||
Ok(UpdateStatus::AlreadyUptodate) => Ok("termscp is already up to date".to_string()),
|
Ok(UpdateStatus::AlreadyUptodate) => Ok("termscp is already up to date".to_string()),
|
||||||
Ok(UpdateStatus::UpdateInstalled(v)) => {
|
Ok(UpdateStatus::UpdateInstalled(v)) => {
|
||||||
|
if get_config_client()
|
||||||
|
.map(|x| x.get_notifications())
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
Notification::update_installed(v.as_str());
|
||||||
|
}
|
||||||
Ok(format!("termscp has been updated to version {}", v))
|
Ok(format!("termscp has been updated to version {}", v))
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => {
|
||||||
|
if get_config_client()
|
||||||
|
.map(|x| x.get_notifications())
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
Notification::update_failed(err.to_string());
|
||||||
|
}
|
||||||
|
Err(err.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,3 +103,19 @@ fn get_config_dir() -> Result<PathBuf, String> {
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### get_config_client
|
||||||
|
///
|
||||||
|
/// Get configuration client
|
||||||
|
fn get_config_client() -> Option<ConfigClient> {
|
||||||
|
match get_config_dir() {
|
||||||
|
Err(_) => None,
|
||||||
|
Ok(dir) => {
|
||||||
|
let (cfg_path, ssh_key_dir) = environment::get_config_paths(dir.as_path());
|
||||||
|
match ConfigClient::new(cfg_path.as_path(), ssh_key_dir.as_path()) {
|
||||||
|
Err(_) => None,
|
||||||
|
Ok(c) => Some(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
*/
|
*/
|
||||||
// Locals
|
// Locals
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
params::UserConfig,
|
params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD},
|
||||||
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
||||||
};
|
};
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
|
@ -254,6 +254,37 @@ impl ConfigClient {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### get_notifications
|
||||||
|
///
|
||||||
|
/// Get value of `notifications`
|
||||||
|
pub fn get_notifications(&self) -> bool {
|
||||||
|
self.config.user_interface.notifications.unwrap_or(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### set_notifications
|
||||||
|
///
|
||||||
|
/// Set new value for `notifications`
|
||||||
|
pub fn set_notifications(&mut self, value: bool) {
|
||||||
|
self.config.user_interface.notifications = Some(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_notification_threshold
|
||||||
|
///
|
||||||
|
/// Get value of `notification_threshold`
|
||||||
|
pub fn get_notification_threshold(&self) -> u64 {
|
||||||
|
self.config
|
||||||
|
.user_interface
|
||||||
|
.notification_threshold
|
||||||
|
.unwrap_or(DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### set_notification_threshold
|
||||||
|
///
|
||||||
|
/// Set new value for `notification_threshold`
|
||||||
|
pub fn set_notification_threshold(&mut self, value: u64) {
|
||||||
|
self.config.user_interface.notification_threshold = Some(value);
|
||||||
|
}
|
||||||
|
|
||||||
// SSH Keys
|
// SSH Keys
|
||||||
|
|
||||||
/// ### save_ssh_key
|
/// ### save_ssh_key
|
||||||
|
@ -657,6 +688,37 @@ mod tests {
|
||||||
assert_eq!(client.get_remote_file_fmt(), None);
|
assert_eq!(client.get_remote_file_fmt(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_config_notifications() {
|
||||||
|
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
|
||||||
|
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_notifications(), true); // Null ?
|
||||||
|
client.set_notifications(true);
|
||||||
|
assert_eq!(client.get_notifications(), true);
|
||||||
|
client.set_notifications(false);
|
||||||
|
assert_eq!(client.get_notifications(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_config_remote_notification_threshold() {
|
||||||
|
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
|
||||||
|
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_notification_threshold(),
|
||||||
|
DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD
|
||||||
|
); // Null ?
|
||||||
|
client.set_notification_threshold(1024);
|
||||||
|
assert_eq!(client.get_notification_threshold(), 1024);
|
||||||
|
client.set_notification_threshold(64);
|
||||||
|
assert_eq!(client.get_notification_threshold(), 64);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_system_config_ssh_keys() {
|
fn test_system_config_ssh_keys() {
|
||||||
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
|
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
|
||||||
|
|
|
@ -32,5 +32,6 @@ pub mod config_client;
|
||||||
pub mod environment;
|
pub mod environment;
|
||||||
pub(self) mod keys;
|
pub(self) mod keys;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
|
pub mod notifications;
|
||||||
pub mod sshkey_storage;
|
pub mod sshkey_storage;
|
||||||
pub mod theme_provider;
|
pub mod theme_provider;
|
||||||
|
|
82
src/system/notifications.rs
Normal file
82
src/system/notifications.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
//! # Notifications
|
||||||
|
//!
|
||||||
|
//! This module exposes the function to send notifications to the guest OS
|
||||||
|
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
use notify_rust::Hint;
|
||||||
|
use notify_rust::{Notification as OsNotification, Timeout};
|
||||||
|
|
||||||
|
/// ## Notification
|
||||||
|
///
|
||||||
|
/// A notification helper which provides all the functions to send the available notifications for termscp
|
||||||
|
pub struct Notification;
|
||||||
|
|
||||||
|
impl Notification {
|
||||||
|
/// ### transfer_completed
|
||||||
|
///
|
||||||
|
/// Notify a transfer has been completed with success
|
||||||
|
pub fn transfer_completed<S: AsRef<str>>(body: S) {
|
||||||
|
Self::notify(
|
||||||
|
"Transfer completed ✅",
|
||||||
|
body.as_ref(),
|
||||||
|
Some("transfer.complete"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### transfer_error
|
||||||
|
///
|
||||||
|
/// Notify a transfer has failed
|
||||||
|
pub fn transfer_error<S: AsRef<str>>(body: S) {
|
||||||
|
Self::notify("Transfer failed ❌", body.as_ref(), Some("transfer.error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### update_available
|
||||||
|
///
|
||||||
|
/// Notify a new version of termscp is available for download
|
||||||
|
pub fn update_available<S: AsRef<str>>(version: S) {
|
||||||
|
Self::notify(
|
||||||
|
"New version available ⬇️",
|
||||||
|
format!("termscp {} is now available for download", version.as_ref()).as_str(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### update_installed
|
||||||
|
///
|
||||||
|
/// Notify the update has been correctly installed
|
||||||
|
pub fn update_installed<S: AsRef<str>>(version: S) {
|
||||||
|
Self::notify(
|
||||||
|
"Update installed 🎉",
|
||||||
|
format!("termscp {} has been installed! Restart termscp to enjoy the latest version of termscp 🙂", version.as_ref()).as_str(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### update_failed
|
||||||
|
///
|
||||||
|
/// Notify the update installation has failed
|
||||||
|
pub fn update_failed<S: AsRef<str>>(err: S) {
|
||||||
|
Self::notify("Update installation failed ❌", err.as_ref(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### notify
|
||||||
|
///
|
||||||
|
/// Notify guest OS with provided Summary, body and optional category
|
||||||
|
/// e.g. Category is supported on FreeBSD/Linux only
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn notify(summary: &str, body: &str, category: Option<&str>) {
|
||||||
|
let mut notification = OsNotification::new();
|
||||||
|
// Set common params
|
||||||
|
notification
|
||||||
|
.appname(env!("CARGO_PKG_NAME"))
|
||||||
|
.summary(summary)
|
||||||
|
.body(body)
|
||||||
|
.timeout(Timeout::Milliseconds(10000));
|
||||||
|
// Set category if any
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
if let Some(category) = category {
|
||||||
|
notification.hint(Hint::Category(category.to_string()));
|
||||||
|
}
|
||||||
|
let _ = notification.show();
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,8 @@
|
||||||
*/
|
*/
|
||||||
use super::{AuthActivity, FileTransferParams, FileTransferProtocol};
|
use super::{AuthActivity, FileTransferParams, FileTransferProtocol};
|
||||||
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
|
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
|
||||||
use crate::system::auto_update::{Update, UpdateStatus};
|
use crate::system::auto_update::{Release, Update, UpdateStatus};
|
||||||
|
use crate::system::notifications::Notification;
|
||||||
|
|
||||||
impl AuthActivity {
|
impl AuthActivity {
|
||||||
/// ### protocol_opt_to_enum
|
/// ### protocol_opt_to_enum
|
||||||
|
@ -155,6 +156,51 @@ impl AuthActivity {
|
||||||
|
|
||||||
// -- update install
|
// -- update install
|
||||||
|
|
||||||
|
/// ### check_for_updates
|
||||||
|
///
|
||||||
|
/// If enabled in configuration, check for updates from Github
|
||||||
|
pub(super) fn check_for_updates(&mut self) {
|
||||||
|
debug!("Check for updates...");
|
||||||
|
// Check version only if unset in the store
|
||||||
|
let ctx = self.context_mut();
|
||||||
|
if !ctx.store().isset(super::STORE_KEY_LATEST_VERSION) {
|
||||||
|
debug!("Version is not set in storage");
|
||||||
|
if ctx.config().get_check_for_updates() {
|
||||||
|
debug!("Check for updates is enabled");
|
||||||
|
// Send request
|
||||||
|
match Update::is_new_version_available() {
|
||||||
|
Ok(Some(Release { version, body })) => {
|
||||||
|
// If some, store version and release notes
|
||||||
|
info!("Latest version is: {}", version);
|
||||||
|
if ctx.config().get_notifications() {
|
||||||
|
// Notify new version available
|
||||||
|
Notification::update_available(version.as_str());
|
||||||
|
}
|
||||||
|
// Store info
|
||||||
|
ctx.store_mut()
|
||||||
|
.set_string(super::STORE_KEY_LATEST_VERSION, version);
|
||||||
|
ctx.store_mut()
|
||||||
|
.set_string(super::STORE_KEY_RELEASE_NOTES, body);
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
info!("Latest version is: {} (current)", env!("CARGO_PKG_VERSION"));
|
||||||
|
// Just set flag as check
|
||||||
|
ctx.store_mut().set(super::STORE_KEY_LATEST_VERSION);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Report error
|
||||||
|
error!("Failed to get latest version: {}", err);
|
||||||
|
self.mount_error(
|
||||||
|
format!("Could not check for new updates: {}", err).as_str(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Check for updates is disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// ### install_update
|
/// ### install_update
|
||||||
///
|
///
|
||||||
/// Install latest termscp version via GUI
|
/// Install latest termscp version via GUI
|
||||||
|
@ -173,9 +219,17 @@ impl AuthActivity {
|
||||||
match result {
|
match result {
|
||||||
Ok(UpdateStatus::AlreadyUptodate) => self.mount_info("termscp is already up to date!"),
|
Ok(UpdateStatus::AlreadyUptodate) => self.mount_info("termscp is already up to date!"),
|
||||||
Ok(UpdateStatus::UpdateInstalled(ver)) => {
|
Ok(UpdateStatus::UpdateInstalled(ver)) => {
|
||||||
|
if self.config().get_notifications() {
|
||||||
|
Notification::update_installed(ver.as_str());
|
||||||
|
}
|
||||||
self.mount_info(format!("termscp has been updated to version {}!", ver))
|
self.mount_info(format!("termscp has been updated to version {}!", ver))
|
||||||
}
|
}
|
||||||
Err(err) => self.mount_error(format!("Could not install update: {}", err)),
|
Err(err) => {
|
||||||
|
if self.config().get_notifications() {
|
||||||
|
Notification::update_failed(err.to_string());
|
||||||
|
}
|
||||||
|
self.mount_error(format!("Could not install update: {}", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,8 +35,8 @@ mod view;
|
||||||
use super::{Activity, Context, ExitReason};
|
use super::{Activity, Context, ExitReason};
|
||||||
use crate::config::themes::Theme;
|
use crate::config::themes::Theme;
|
||||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||||
use crate::system::auto_update::{Release, Update as TermscpUpdate};
|
|
||||||
use crate::system::bookmarks_client::BookmarksClient;
|
use crate::system::bookmarks_client::BookmarksClient;
|
||||||
|
use crate::system::config_client::ConfigClient;
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
use crossterm::event::Event;
|
use crossterm::event::Event;
|
||||||
|
@ -110,45 +110,6 @@ impl AuthActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### on_create
|
|
||||||
///
|
|
||||||
/// If enabled in configuration, check for updates from Github
|
|
||||||
fn check_for_updates(&mut self) {
|
|
||||||
debug!("Check for updates...");
|
|
||||||
// Check version only if unset in the store
|
|
||||||
let ctx: &mut Context = self.context_mut();
|
|
||||||
if !ctx.store().isset(STORE_KEY_LATEST_VERSION) {
|
|
||||||
debug!("Version is not set in storage");
|
|
||||||
if ctx.config().get_check_for_updates() {
|
|
||||||
debug!("Check for updates is enabled");
|
|
||||||
// Send request
|
|
||||||
match TermscpUpdate::is_new_version_available() {
|
|
||||||
Ok(Some(Release { version, body })) => {
|
|
||||||
// If some, store version and release notes
|
|
||||||
info!("Latest version is: {}", version);
|
|
||||||
ctx.store_mut()
|
|
||||||
.set_string(STORE_KEY_LATEST_VERSION, version);
|
|
||||||
ctx.store_mut().set_string(STORE_KEY_RELEASE_NOTES, body);
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
info!("Latest version is: {} (current)", env!("CARGO_PKG_VERSION"));
|
|
||||||
// Just set flag as check
|
|
||||||
ctx.store_mut().set(STORE_KEY_LATEST_VERSION);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
// Report error
|
|
||||||
error!("Failed to get latest version: {}", err);
|
|
||||||
self.mount_error(
|
|
||||||
format!("Could not check for new updates: {}", err).as_str(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("Check for updates is disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### context
|
/// ### context
|
||||||
///
|
///
|
||||||
/// Returns a reference to context
|
/// Returns a reference to context
|
||||||
|
@ -163,6 +124,13 @@ impl AuthActivity {
|
||||||
self.context.as_mut().unwrap()
|
self.context.as_mut().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### config
|
||||||
|
///
|
||||||
|
/// Returns config client reference
|
||||||
|
fn config(&self) -> &ConfigClient {
|
||||||
|
self.context().config()
|
||||||
|
}
|
||||||
|
|
||||||
/// ### theme
|
/// ### theme
|
||||||
///
|
///
|
||||||
/// Returns a reference to theme
|
/// Returns a reference to theme
|
||||||
|
|
|
@ -87,6 +87,13 @@ impl TransferStates {
|
||||||
pub fn aborted(&self) -> bool {
|
pub fn aborted(&self) -> bool {
|
||||||
self.aborted
|
self.aborted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### full_size
|
||||||
|
///
|
||||||
|
/// Returns the size of the entire transfer
|
||||||
|
pub fn full_size(&self) -> usize {
|
||||||
|
self.full.total
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProgressStates {
|
impl Default for ProgressStates {
|
||||||
|
@ -305,6 +312,8 @@ mod test {
|
||||||
assert_eq!(states.aborted(), true);
|
assert_eq!(states.aborted(), true);
|
||||||
states.reset();
|
states.reset();
|
||||||
assert_eq!(states.aborted(), false);
|
assert_eq!(states.aborted(), false);
|
||||||
|
states.full.total = 1024;
|
||||||
|
assert_eq!(states.full_size(), 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -22,12 +22,15 @@
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// Locals
|
// Locals
|
||||||
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
|
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord, TransferPayload};
|
||||||
use crate::filetransfer::ProtocolParams;
|
use crate::filetransfer::ProtocolParams;
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
|
use crate::system::notifications::Notification;
|
||||||
use crate::system::sshkey_storage::SshKeyStorage;
|
use crate::system::sshkey_storage::SshKeyStorage;
|
||||||
|
use crate::utils::fmt::fmt_millis;
|
||||||
use crate::utils::path;
|
use crate::utils::path;
|
||||||
// Ext
|
// Ext
|
||||||
|
use bytesize::ByteSize;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tuirealm::Update;
|
use tuirealm::Update;
|
||||||
|
@ -146,4 +149,86 @@ impl FileTransferActivity {
|
||||||
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
|
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### get_connection_msg
|
||||||
|
///
|
||||||
|
/// Get connection message to show to client
|
||||||
|
pub(super) fn get_connection_msg(params: &ProtocolParams) -> String {
|
||||||
|
match params {
|
||||||
|
ProtocolParams::Generic(params) => {
|
||||||
|
info!(
|
||||||
|
"Client is not connected to remote; connecting to {}:{}",
|
||||||
|
params.address, params.port
|
||||||
|
);
|
||||||
|
format!("Connecting to {}:{}…", params.address, params.port)
|
||||||
|
}
|
||||||
|
ProtocolParams::AwsS3(params) => {
|
||||||
|
info!(
|
||||||
|
"Client is not connected to remote; connecting to {} ({})",
|
||||||
|
params.bucket_name, params.region
|
||||||
|
);
|
||||||
|
format!("Connecting to {}…", params.bucket_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### notify_transfer_completed
|
||||||
|
///
|
||||||
|
/// Send notification regarding transfer completed
|
||||||
|
/// The notification is sent only when these conditions are satisfied:
|
||||||
|
///
|
||||||
|
/// - notifications are enabled
|
||||||
|
/// - transfer size is greater or equal than notification threshold
|
||||||
|
pub(super) fn notify_transfer_completed(&self, payload: &TransferPayload) {
|
||||||
|
if self.config().get_notifications()
|
||||||
|
&& self.config().get_notification_threshold() as usize <= self.transfer.full_size()
|
||||||
|
{
|
||||||
|
Notification::transfer_completed(self.transfer_completed_msg(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### notify_transfer_error
|
||||||
|
///
|
||||||
|
/// Send notification regarding transfer error
|
||||||
|
/// The notification is sent only when these conditions are satisfied:
|
||||||
|
///
|
||||||
|
/// - notifications are enabled
|
||||||
|
/// - transfer size is greater or equal than notification threshold
|
||||||
|
pub(super) fn notify_transfer_error(&self, msg: &str) {
|
||||||
|
if self.config().get_notifications()
|
||||||
|
&& self.config().get_notification_threshold() as usize <= self.transfer.full_size()
|
||||||
|
{
|
||||||
|
Notification::transfer_error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer_completed_msg(&self, payload: &TransferPayload) -> String {
|
||||||
|
let transfer_stats = format!(
|
||||||
|
"took {} seconds; at {}/s",
|
||||||
|
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||||
|
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||||
|
);
|
||||||
|
match payload {
|
||||||
|
TransferPayload::File(file) => {
|
||||||
|
format!(
|
||||||
|
"File \"{}\" has been successfully transferred ({})",
|
||||||
|
file.name, transfer_stats
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TransferPayload::Any(entry) => {
|
||||||
|
format!(
|
||||||
|
"\"{}\" has been successfully transferred ({})",
|
||||||
|
entry.get_name(),
|
||||||
|
transfer_stats
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TransferPayload::Many(entries) => {
|
||||||
|
format!(
|
||||||
|
"{} files has been successfully transferred ({})",
|
||||||
|
entries.len(),
|
||||||
|
transfer_stats
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ pub(self) mod view;
|
||||||
// locals
|
// locals
|
||||||
use super::{Activity, Context, ExitReason};
|
use super::{Activity, Context, ExitReason};
|
||||||
use crate::config::themes::Theme;
|
use crate::config::themes::Theme;
|
||||||
use crate::filetransfer::{FileTransfer, FileTransferProtocol, ProtocolParams};
|
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
||||||
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
|
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
|
||||||
use crate::fs::explorer::FileExplorer;
|
use crate::fs::explorer::FileExplorer;
|
||||||
use crate::fs::FsEntry;
|
use crate::fs::FsEntry;
|
||||||
|
@ -239,28 +239,6 @@ impl FileTransferActivity {
|
||||||
fn theme(&self) -> &Theme {
|
fn theme(&self) -> &Theme {
|
||||||
self.context().theme_provider().theme()
|
self.context().theme_provider().theme()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get_connection_msg
|
|
||||||
///
|
|
||||||
/// Get connection message to show to client
|
|
||||||
fn get_connection_msg(params: &ProtocolParams) -> String {
|
|
||||||
match params {
|
|
||||||
ProtocolParams::Generic(params) => {
|
|
||||||
info!(
|
|
||||||
"Client is not connected to remote; connecting to {}:{}",
|
|
||||||
params.address, params.port
|
|
||||||
);
|
|
||||||
format!("Connecting to {}:{}…", params.address, params.port)
|
|
||||||
}
|
|
||||||
ProtocolParams::AwsS3(params) => {
|
|
||||||
info!(
|
|
||||||
"Client is not connected to remote; connecting to {} ({})",
|
|
||||||
params.bucket_name, params.region
|
|
||||||
);
|
|
||||||
format!("Connecting to {}…", params.bucket_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -206,17 +206,27 @@ impl FileTransferActivity {
|
||||||
dst_name: Option<String>,
|
dst_name: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Use different method based on payload
|
// Use different method based on payload
|
||||||
match payload {
|
let result = match payload {
|
||||||
TransferPayload::Any(entry) => {
|
TransferPayload::Any(ref entry) => {
|
||||||
self.filetransfer_send_any(&entry, curr_remote_path, dst_name)
|
self.filetransfer_send_any(entry, curr_remote_path, dst_name)
|
||||||
}
|
}
|
||||||
TransferPayload::File(file) => {
|
TransferPayload::File(ref file) => {
|
||||||
self.filetransfer_send_file(&file, curr_remote_path, dst_name)
|
self.filetransfer_send_file(file, curr_remote_path, dst_name)
|
||||||
}
|
}
|
||||||
TransferPayload::Many(entries) => {
|
TransferPayload::Many(ref entries) => {
|
||||||
self.filetransfer_send_many(entries, curr_remote_path)
|
self.filetransfer_send_many(entries, curr_remote_path)
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
// Notify
|
||||||
|
match &result {
|
||||||
|
Ok(_) => {
|
||||||
|
self.notify_transfer_completed(&payload);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.notify_transfer_error(e.as_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### filetransfer_send_file
|
/// ### filetransfer_send_file
|
||||||
|
@ -268,10 +278,10 @@ impl FileTransferActivity {
|
||||||
// Mount progress bar
|
// Mount progress bar
|
||||||
self.mount_progress_bar(format!("Uploading {}…", entry.get_abs_path().display()));
|
self.mount_progress_bar(format!("Uploading {}…", entry.get_abs_path().display()));
|
||||||
// Send recurse
|
// Send recurse
|
||||||
self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
|
let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
|
||||||
// Umount progress bar
|
// Umount progress bar
|
||||||
self.umount_progress_bar();
|
self.umount_progress_bar();
|
||||||
Ok(())
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### filetransfer_send_many
|
/// ### filetransfer_send_many
|
||||||
|
@ -279,7 +289,7 @@ impl FileTransferActivity {
|
||||||
/// Send many entries to remote
|
/// Send many entries to remote
|
||||||
fn filetransfer_send_many(
|
fn filetransfer_send_many(
|
||||||
&mut self,
|
&mut self,
|
||||||
entries: Vec<FsEntry>,
|
entries: &[FsEntry],
|
||||||
curr_remote_path: &Path,
|
curr_remote_path: &Path,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Reset states
|
// Reset states
|
||||||
|
@ -293,12 +303,14 @@ impl FileTransferActivity {
|
||||||
// Mount progress bar
|
// Mount progress bar
|
||||||
self.mount_progress_bar(format!("Uploading {} entries…", entries.len()));
|
self.mount_progress_bar(format!("Uploading {} entries…", entries.len()));
|
||||||
// Send recurse
|
// Send recurse
|
||||||
entries
|
let result = entries
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|x| self.filetransfer_send_recurse(x, curr_remote_path, None));
|
.map(|x| self.filetransfer_send_recurse(x, curr_remote_path, None))
|
||||||
|
.find(|x| x.is_err())
|
||||||
|
.unwrap_or(Ok(()));
|
||||||
// Umount progress bar
|
// Umount progress bar
|
||||||
self.umount_progress_bar();
|
self.umount_progress_bar();
|
||||||
Ok(())
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filetransfer_send_recurse(
|
fn filetransfer_send_recurse(
|
||||||
|
@ -306,7 +318,7 @@ impl FileTransferActivity {
|
||||||
entry: &FsEntry,
|
entry: &FsEntry,
|
||||||
curr_remote_path: &Path,
|
curr_remote_path: &Path,
|
||||||
dst_name: Option<String>,
|
dst_name: Option<String>,
|
||||||
) {
|
) -> Result<(), String> {
|
||||||
// Write popup
|
// Write popup
|
||||||
let file_name: String = match entry {
|
let file_name: String = match entry {
|
||||||
FsEntry::Directory(dir) => dir.name.clone(),
|
FsEntry::Directory(dir) => dir.name.clone(),
|
||||||
|
@ -320,44 +332,42 @@ impl FileTransferActivity {
|
||||||
};
|
};
|
||||||
remote_path.push(remote_file_name);
|
remote_path.push(remote_file_name);
|
||||||
// Match entry
|
// Match entry
|
||||||
match entry {
|
let result: Result<(), String> = match entry {
|
||||||
FsEntry::File(file) => {
|
FsEntry::File(file) => {
|
||||||
if let Err(err) = self.filetransfer_send_one(file, remote_path.as_path(), file_name)
|
match self.filetransfer_send_one(file, remote_path.as_path(), file_name) {
|
||||||
{
|
Err(err) => {
|
||||||
// Log error
|
// If transfer was abrupted or there was an IO error on remote, remove file
|
||||||
self.log_and_alert(
|
if matches!(
|
||||||
LogLevel::Error,
|
err,
|
||||||
format!("Failed to upload file {}: {}", file.name, err),
|
TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_)
|
||||||
);
|
) {
|
||||||
// If transfer was abrupted or there was an IO error on remote, remove file
|
// Stat file on remote and remove it if exists
|
||||||
if matches!(
|
match self.client.stat(remote_path.as_path()) {
|
||||||
err,
|
Err(err) => self.log(
|
||||||
TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_)
|
LogLevel::Error,
|
||||||
) {
|
format!(
|
||||||
// Stat file on remote and remove it if exists
|
"Could not remove created file {}: {}",
|
||||||
match self.client.stat(remote_path.as_path()) {
|
remote_path.display(),
|
||||||
Err(err) => self.log(
|
err
|
||||||
LogLevel::Error,
|
),
|
||||||
format!(
|
|
||||||
"Could not remove created file {}: {}",
|
|
||||||
remote_path.display(),
|
|
||||||
err
|
|
||||||
),
|
),
|
||||||
),
|
Ok(entry) => {
|
||||||
Ok(entry) => {
|
if let Err(err) = self.client.remove(&entry) {
|
||||||
if let Err(err) = self.client.remove(&entry) {
|
self.log(
|
||||||
self.log(
|
LogLevel::Error,
|
||||||
LogLevel::Error,
|
format!(
|
||||||
format!(
|
"Could not remove created file {}: {}",
|
||||||
"Could not remove created file {}: {}",
|
remote_path.display(),
|
||||||
remote_path.display(),
|
err
|
||||||
err
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(err.to_string())
|
||||||
}
|
}
|
||||||
|
Ok(_) => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FsEntry::Directory(dir) => {
|
FsEntry::Directory(dir) => {
|
||||||
|
@ -387,7 +397,7 @@ impl FileTransferActivity {
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return Err(err.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Get files in dir
|
// Get files in dir
|
||||||
|
@ -400,8 +410,13 @@ impl FileTransferActivity {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Send entry; name is always None after first call
|
// Send entry; name is always None after first call
|
||||||
self.filetransfer_send_recurse(entry, remote_path.as_path(), None);
|
if let Err(err) =
|
||||||
|
self.filetransfer_send_recurse(entry, remote_path.as_path(), None)
|
||||||
|
{
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
|
@ -412,10 +427,11 @@ impl FileTransferActivity {
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
Err(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
// Scan dir on remote
|
// Scan dir on remote
|
||||||
self.reload_remote_dir();
|
self.reload_remote_dir();
|
||||||
// If aborted; show popup
|
// If aborted; show popup
|
||||||
|
@ -426,6 +442,7 @@ impl FileTransferActivity {
|
||||||
format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()),
|
format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### filetransfer_send_file
|
/// ### filetransfer_send_file
|
||||||
|
@ -613,11 +630,23 @@ impl FileTransferActivity {
|
||||||
local_path: &Path,
|
local_path: &Path,
|
||||||
dst_name: Option<String>,
|
dst_name: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
match payload {
|
let result = match payload {
|
||||||
TransferPayload::Any(entry) => self.filetransfer_recv_any(&entry, local_path, dst_name),
|
TransferPayload::Any(ref entry) => {
|
||||||
TransferPayload::File(file) => self.filetransfer_recv_file(&file, local_path),
|
self.filetransfer_recv_any(entry, local_path, dst_name)
|
||||||
TransferPayload::Many(entries) => self.filetransfer_recv_many(entries, local_path),
|
}
|
||||||
|
TransferPayload::File(ref file) => self.filetransfer_recv_file(file, local_path),
|
||||||
|
TransferPayload::Many(ref entries) => self.filetransfer_recv_many(entries, local_path),
|
||||||
|
};
|
||||||
|
// Notify
|
||||||
|
match &result {
|
||||||
|
Ok(_) => {
|
||||||
|
self.notify_transfer_completed(&payload);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.notify_transfer_error(e.as_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### filetransfer_recv_any
|
/// ### filetransfer_recv_any
|
||||||
|
@ -639,10 +668,10 @@ impl FileTransferActivity {
|
||||||
// Mount progress bar
|
// Mount progress bar
|
||||||
self.mount_progress_bar(format!("Downloading {}…", entry.get_abs_path().display()));
|
self.mount_progress_bar(format!("Downloading {}…", entry.get_abs_path().display()));
|
||||||
// Receive
|
// Receive
|
||||||
self.filetransfer_recv_recurse(entry, local_path, dst_name);
|
let result = self.filetransfer_recv_recurse(entry, local_path, dst_name);
|
||||||
// Umount progress bar
|
// Umount progress bar
|
||||||
self.umount_progress_bar();
|
self.umount_progress_bar();
|
||||||
Ok(())
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### filetransfer_recv_file
|
/// ### filetransfer_recv_file
|
||||||
|
@ -669,7 +698,7 @@ impl FileTransferActivity {
|
||||||
/// Send many entries to remote
|
/// Send many entries to remote
|
||||||
fn filetransfer_recv_many(
|
fn filetransfer_recv_many(
|
||||||
&mut self,
|
&mut self,
|
||||||
entries: Vec<FsEntry>,
|
entries: &[FsEntry],
|
||||||
curr_remote_path: &Path,
|
curr_remote_path: &Path,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Reset states
|
// Reset states
|
||||||
|
@ -683,12 +712,14 @@ impl FileTransferActivity {
|
||||||
// Mount progress bar
|
// Mount progress bar
|
||||||
self.mount_progress_bar(format!("Downloading {} entries…", entries.len()));
|
self.mount_progress_bar(format!("Downloading {} entries…", entries.len()));
|
||||||
// Send recurse
|
// Send recurse
|
||||||
entries
|
let result = entries
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|x| self.filetransfer_recv_recurse(x, curr_remote_path, None));
|
.map(|x| self.filetransfer_recv_recurse(x, curr_remote_path, None))
|
||||||
|
.find(|x| x.is_err())
|
||||||
|
.unwrap_or(Ok(()));
|
||||||
// Umount progress bar
|
// Umount progress bar
|
||||||
self.umount_progress_bar();
|
self.umount_progress_bar();
|
||||||
Ok(())
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filetransfer_recv_recurse(
|
fn filetransfer_recv_recurse(
|
||||||
|
@ -696,14 +727,14 @@ impl FileTransferActivity {
|
||||||
entry: &FsEntry,
|
entry: &FsEntry,
|
||||||
local_path: &Path,
|
local_path: &Path,
|
||||||
dst_name: Option<String>,
|
dst_name: Option<String>,
|
||||||
) {
|
) -> Result<(), String> {
|
||||||
// Write popup
|
// Write popup
|
||||||
let file_name: String = match entry {
|
let file_name: String = match entry {
|
||||||
FsEntry::Directory(dir) => dir.name.clone(),
|
FsEntry::Directory(dir) => dir.name.clone(),
|
||||||
FsEntry::File(file) => file.name.clone(),
|
FsEntry::File(file) => file.name.clone(),
|
||||||
};
|
};
|
||||||
// Match entry
|
// Match entry
|
||||||
match entry {
|
let result: Result<(), String> = match entry {
|
||||||
FsEntry::File(file) => {
|
FsEntry::File(file) => {
|
||||||
// Get local file
|
// Get local file
|
||||||
let mut local_file_path: PathBuf = PathBuf::from(local_path);
|
let mut local_file_path: PathBuf = PathBuf::from(local_path);
|
||||||
|
@ -716,10 +747,6 @@ impl FileTransferActivity {
|
||||||
if let Err(err) =
|
if let Err(err) =
|
||||||
self.filetransfer_recv_one(local_file_path.as_path(), file, file_name)
|
self.filetransfer_recv_one(local_file_path.as_path(), file, file_name)
|
||||||
{
|
{
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not download file {}: {}", file.name, err),
|
|
||||||
);
|
|
||||||
// If transfer was abrupted or there was an IO error on remote, remove file
|
// If transfer was abrupted or there was an IO error on remote, remove file
|
||||||
if matches!(
|
if matches!(
|
||||||
err,
|
err,
|
||||||
|
@ -749,6 +776,9 @@ impl FileTransferActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(err.to_string())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FsEntry::Directory(dir) => {
|
FsEntry::Directory(dir) => {
|
||||||
|
@ -798,12 +828,15 @@ impl FileTransferActivity {
|
||||||
}
|
}
|
||||||
// Receive entry; name is always None after first call
|
// Receive entry; name is always None after first call
|
||||||
// Local path becomes local_dir_path
|
// Local path becomes local_dir_path
|
||||||
self.filetransfer_recv_recurse(
|
if let Err(err) = self.filetransfer_recv_recurse(
|
||||||
entry,
|
entry,
|
||||||
local_dir_path.as_path(),
|
local_dir_path.as_path(),
|
||||||
None,
|
None,
|
||||||
);
|
) {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
|
@ -814,6 +847,7 @@ impl FileTransferActivity {
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
Err(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -826,10 +860,11 @@ impl FileTransferActivity {
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
Err(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
// Reload directory on local
|
// Reload directory on local
|
||||||
self.reload_local_dir();
|
self.reload_local_dir();
|
||||||
// if aborted; show alert
|
// if aborted; show alert
|
||||||
|
@ -843,6 +878,7 @@ impl FileTransferActivity {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### filetransfer_recv_one
|
/// ### filetransfer_recv_one
|
||||||
|
|
|
@ -58,6 +58,8 @@ const COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE: &str = "RADIO_PROMPT_ON_FILE_REPLA
|
||||||
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
|
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
|
||||||
const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_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_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT";
|
||||||
|
const COMPONENT_RADIO_NOTIFICATIONS_ENABLED: &str = "RADIO_NOTIFICATIONS_ENABLED";
|
||||||
|
const COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD: &str = "INPUT_NOTIFICATIONS_THRESHOLD";
|
||||||
// -- ssh keys
|
// -- ssh keys
|
||||||
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";
|
||||||
|
|
|
@ -40,11 +40,13 @@ use super::{
|
||||||
COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
|
COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
|
||||||
COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
|
COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
|
||||||
COMPONENT_COLOR_TRANSFER_STATUS_SYNC, COMPONENT_INPUT_LOCAL_FILE_FMT,
|
COMPONENT_COLOR_TRANSFER_STATUS_SYNC, COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||||
COMPONENT_INPUT_REMOTE_FILE_FMT, COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME,
|
COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||||
COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR,
|
||||||
COMPONENT_RADIO_DEL_SSH_KEY, COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES,
|
COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY,
|
||||||
COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE,
|
COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES,
|
||||||
COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP,
|
COMPONENT_RADIO_NOTIFICATIONS_ENABLED, COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
|
||||||
|
COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR,
|
||||||
|
COMPONENT_TEXT_HELP,
|
||||||
};
|
};
|
||||||
use crate::ui::keymap::*;
|
use crate::ui::keymap::*;
|
||||||
use crate::utils::parser::parse_color;
|
use crate::utils::parser::parse_color;
|
||||||
|
@ -103,10 +105,26 @@ impl SetupActivity {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_DOWN => {
|
(COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_DOWN => {
|
||||||
|
self.view.active(COMPONENT_RADIO_NOTIFICATIONS_ENABLED);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
(COMPONENT_RADIO_NOTIFICATIONS_ENABLED, key) if key == &MSG_KEY_DOWN => {
|
||||||
|
self.view.active(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, key) if key == &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_NOTIFICATIONS_THRESHOLD, key) if key == &MSG_KEY_UP => {
|
||||||
|
self.view.active(COMPONENT_RADIO_NOTIFICATIONS_ENABLED);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
(COMPONENT_RADIO_NOTIFICATIONS_ENABLED, key) if key == &MSG_KEY_UP => {
|
||||||
|
self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT);
|
||||||
|
None
|
||||||
|
}
|
||||||
(COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_UP => {
|
(COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_UP => {
|
||||||
self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT);
|
self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT);
|
||||||
None
|
None
|
||||||
|
@ -136,7 +154,7 @@ impl SetupActivity {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_INPUT_TEXT_EDITOR, key) if key == &MSG_KEY_UP => {
|
(COMPONENT_INPUT_TEXT_EDITOR, key) if key == &MSG_KEY_UP => {
|
||||||
self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT);
|
self.view.active(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
// Error <ENTER> or <ESC>
|
// Error <ENTER> or <ESC>
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
use super::{Context, SetupActivity};
|
use super::{Context, SetupActivity};
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
use crate::fs::explorer::GroupDirs;
|
use crate::fs::explorer::GroupDirs;
|
||||||
|
use crate::ui::components::bytes::{Bytes, BytesPropsBuilder};
|
||||||
use crate::utils::ui::draw_area_in;
|
use crate::utils::ui::draw_area_in;
|
||||||
// Ext
|
// Ext
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -143,8 +144,8 @@ impl SetupActivity {
|
||||||
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
|
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
InputPropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_foreground(Color::LightBlue)
|
.with_foreground(Color::LightGreen)
|
||||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
||||||
.with_label("File formatter syntax (local)", Alignment::Left)
|
.with_label("File formatter syntax (local)", Alignment::Left)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
|
@ -153,12 +154,35 @@ impl SetupActivity {
|
||||||
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
|
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||||
Box::new(Input::new(
|
Box::new(Input::new(
|
||||||
InputPropsBuilder::default()
|
InputPropsBuilder::default()
|
||||||
.with_foreground(Color::LightGreen)
|
.with_foreground(Color::LightCyan)
|
||||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
|
||||||
.with_label("File formatter syntax (remote)", Alignment::Left)
|
.with_label("File formatter syntax (remote)", Alignment::Left)
|
||||||
.build(),
|
.build(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
self.view.mount(
|
||||||
|
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
|
||||||
|
Box::new(Radio::new(
|
||||||
|
RadioPropsBuilder::default()
|
||||||
|
.with_color(Color::LightRed)
|
||||||
|
.with_inverted_color(Color::Black)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
|
||||||
|
.with_title("Enable notifications?", Alignment::Left)
|
||||||
|
.with_options(&[String::from("Yes"), String::from("No")])
|
||||||
|
.rewind(true)
|
||||||
|
.build(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
self.view.mount(
|
||||||
|
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
|
||||||
|
Box::new(Bytes::new(
|
||||||
|
BytesPropsBuilder::default()
|
||||||
|
.with_foreground(Color::LightYellow)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
|
||||||
|
.with_label("Notifications: minimum transfer size", Alignment::Left)
|
||||||
|
.build(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
// Load values
|
// Load values
|
||||||
self.load_input_values();
|
self.load_input_values();
|
||||||
}
|
}
|
||||||
|
@ -173,7 +197,7 @@ impl SetupActivity {
|
||||||
.constraints(
|
.constraints(
|
||||||
[
|
[
|
||||||
Constraint::Length(3), // Current tab
|
Constraint::Length(3), // Current tab
|
||||||
Constraint::Length(21), // Main body
|
Constraint::Length(18), // Main body
|
||||||
Constraint::Length(3), // Help footer
|
Constraint::Length(3), // Help footer
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
|
@ -182,8 +206,13 @@ impl SetupActivity {
|
||||||
// Render common widget
|
// Render common widget
|
||||||
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
|
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
|
||||||
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
|
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
|
||||||
// Make chunks
|
// Make chunks (two columns)
|
||||||
let ui_cfg_chunks = Layout::default()
|
let ui_cfg_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||||
|
.split(chunks[1]);
|
||||||
|
// Column 1
|
||||||
|
let ui_cfg_chunks_col1 = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(
|
.constraints(
|
||||||
[
|
[
|
||||||
|
@ -193,31 +222,65 @@ impl SetupActivity {
|
||||||
Constraint::Length(3), // Updates tab
|
Constraint::Length(3), // Updates tab
|
||||||
Constraint::Length(3), // Prompt file replace
|
Constraint::Length(3), // Prompt file replace
|
||||||
Constraint::Length(3), // Group dirs
|
Constraint::Length(3), // Group dirs
|
||||||
Constraint::Length(3), // Local Format input
|
|
||||||
Constraint::Length(3), // Remote Format input
|
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
.split(chunks[1]);
|
.split(ui_cfg_chunks[0]);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks[0]);
|
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks_col1[0]);
|
||||||
|
self.view.render(
|
||||||
|
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
|
||||||
|
f,
|
||||||
|
ui_cfg_chunks_col1[1],
|
||||||
|
);
|
||||||
|
self.view.render(
|
||||||
|
super::COMPONENT_RADIO_HIDDEN_FILES,
|
||||||
|
f,
|
||||||
|
ui_cfg_chunks_col1[2],
|
||||||
|
);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, f, ui_cfg_chunks[1]);
|
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks_col1[3]);
|
||||||
self.view
|
|
||||||
.render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]);
|
|
||||||
self.view
|
|
||||||
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]);
|
|
||||||
self.view.render(
|
self.view.render(
|
||||||
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
|
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
|
||||||
f,
|
f,
|
||||||
ui_cfg_chunks[4],
|
ui_cfg_chunks_col1[4],
|
||||||
);
|
);
|
||||||
self.view
|
self.view
|
||||||
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[5]);
|
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks_col1[5]);
|
||||||
self.view
|
// Column 2
|
||||||
.render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[6]);
|
let ui_cfg_chunks_col2 = Layout::default()
|
||||||
self.view
|
.direction(Direction::Vertical)
|
||||||
.render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[7]);
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(3), // Local Format input
|
||||||
|
Constraint::Length(3), // Remote Format input
|
||||||
|
Constraint::Length(3), // Notifications enabled
|
||||||
|
Constraint::Length(3), // Notifications threshold
|
||||||
|
Constraint::Length(1), // Filler
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(ui_cfg_chunks[1]);
|
||||||
|
self.view.render(
|
||||||
|
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
|
||||||
|
f,
|
||||||
|
ui_cfg_chunks_col2[0],
|
||||||
|
);
|
||||||
|
self.view.render(
|
||||||
|
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
|
||||||
|
f,
|
||||||
|
ui_cfg_chunks_col2[1],
|
||||||
|
);
|
||||||
|
self.view.render(
|
||||||
|
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
|
||||||
|
f,
|
||||||
|
ui_cfg_chunks_col2[2],
|
||||||
|
);
|
||||||
|
self.view.render(
|
||||||
|
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
|
||||||
|
f,
|
||||||
|
ui_cfg_chunks_col2[3],
|
||||||
|
);
|
||||||
// Popups
|
// Popups
|
||||||
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
|
||||||
if props.visible {
|
if props.visible {
|
||||||
|
@ -341,6 +404,31 @@ impl SetupActivity {
|
||||||
.view
|
.view
|
||||||
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
|
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
|
||||||
}
|
}
|
||||||
|
// Notifications enabled
|
||||||
|
if let Some(props) = self
|
||||||
|
.view
|
||||||
|
.get_props(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
|
||||||
|
{
|
||||||
|
let enabled: usize = match self.config().get_notifications() {
|
||||||
|
true => 0,
|
||||||
|
false => 1,
|
||||||
|
};
|
||||||
|
let props = RadioPropsBuilder::from(props).with_value(enabled).build();
|
||||||
|
let _ = self
|
||||||
|
.view
|
||||||
|
.update(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED, props);
|
||||||
|
}
|
||||||
|
// Notifications threshold
|
||||||
|
if let Some(props) = self
|
||||||
|
.view
|
||||||
|
.get_props(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
|
||||||
|
{
|
||||||
|
let value: u64 = self.config().get_notification_threshold();
|
||||||
|
let props = BytesPropsBuilder::from(props).with_value(value).build();
|
||||||
|
let _ = self
|
||||||
|
.view
|
||||||
|
.update(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, props);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### collect_input_values
|
/// ### collect_input_values
|
||||||
|
@ -404,5 +492,17 @@ impl SetupActivity {
|
||||||
};
|
};
|
||||||
self.config_mut().set_group_dirs(dirs);
|
self.config_mut().set_group_dirs(dirs);
|
||||||
}
|
}
|
||||||
|
if let Some(Payload::One(Value::Usize(opt))) = self
|
||||||
|
.view
|
||||||
|
.get_state(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
|
||||||
|
{
|
||||||
|
self.config_mut().set_notifications(opt == 0);
|
||||||
|
}
|
||||||
|
if let Some(Payload::One(Value::U64(bytes))) = self
|
||||||
|
.view
|
||||||
|
.get_state(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
|
||||||
|
{
|
||||||
|
self.config_mut().set_notification_threshold(bytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
310
src/ui/components/bytes.rs
Normal file
310
src/ui/components/bytes.rs
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
//! ## Bytes
|
||||||
|
//!
|
||||||
|
//! `Bytes` component extends an `Input` component in order to provide an input type for byte size.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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::utils::fmt::fmt_bytes;
|
||||||
|
use crate::utils::parser::parse_bytesize;
|
||||||
|
// ext
|
||||||
|
use tui_realm_stdlib::{Input, InputPropsBuilder};
|
||||||
|
use tuirealm::event::Event;
|
||||||
|
use tuirealm::props::{Alignment, Props, PropsBuilder};
|
||||||
|
use tuirealm::tui::{
|
||||||
|
layout::Rect,
|
||||||
|
style::Color,
|
||||||
|
widgets::{BorderType, Borders},
|
||||||
|
};
|
||||||
|
use tuirealm::{Component, Frame, Msg, Payload, Value};
|
||||||
|
|
||||||
|
// -- props
|
||||||
|
|
||||||
|
/// ## BytesPropsBuilder
|
||||||
|
///
|
||||||
|
/// A wrapper around an `InputPropsBuilder`
|
||||||
|
pub struct BytesPropsBuilder {
|
||||||
|
puppet: InputPropsBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BytesPropsBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
puppet: InputPropsBuilder::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropsBuilder for BytesPropsBuilder {
|
||||||
|
fn build(&mut self) -> Props {
|
||||||
|
self.puppet.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hidden(&mut self) -> &mut Self {
|
||||||
|
self.puppet.hidden();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible(&mut self) -> &mut Self {
|
||||||
|
self.puppet.visible();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Props> for BytesPropsBuilder {
|
||||||
|
fn from(props: Props) -> Self {
|
||||||
|
BytesPropsBuilder {
|
||||||
|
puppet: InputPropsBuilder::from(props),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BytesPropsBuilder {
|
||||||
|
/// ### with_borders
|
||||||
|
///
|
||||||
|
/// Set component borders style
|
||||||
|
pub fn with_borders(
|
||||||
|
&mut self,
|
||||||
|
borders: Borders,
|
||||||
|
variant: BorderType,
|
||||||
|
color: Color,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.puppet.with_borders(borders, variant, color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### with_label
|
||||||
|
///
|
||||||
|
/// Set input label
|
||||||
|
pub fn with_label<S: AsRef<str>>(&mut self, label: S, alignment: Alignment) -> &mut Self {
|
||||||
|
self.puppet.with_label(label, alignment);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### with_color
|
||||||
|
///
|
||||||
|
/// Set initial value for component
|
||||||
|
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
|
||||||
|
self.puppet.with_foreground(color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### with_color
|
||||||
|
///
|
||||||
|
/// Set initial value for component
|
||||||
|
pub fn with_value(&mut self, val: u64) -> &mut Self {
|
||||||
|
self.puppet.with_value(fmt_bytes(val));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- component
|
||||||
|
|
||||||
|
/// ## Bytes
|
||||||
|
///
|
||||||
|
/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker
|
||||||
|
pub struct Bytes {
|
||||||
|
input: Input,
|
||||||
|
native_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bytes {
|
||||||
|
/// ### new
|
||||||
|
///
|
||||||
|
/// Instantiate a new `Bytes`
|
||||||
|
pub fn new(props: Props) -> Self {
|
||||||
|
// Instantiate a new color picker using input
|
||||||
|
Self {
|
||||||
|
native_color: props.foreground,
|
||||||
|
input: Input::new(props),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### update_colors
|
||||||
|
///
|
||||||
|
/// Update colors to match selected color, with provided one
|
||||||
|
fn update_colors(&mut self, color: Color) {
|
||||||
|
let mut props = self.get_props();
|
||||||
|
props.foreground = color;
|
||||||
|
props.borders.color = color;
|
||||||
|
let _ = self.input.update(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Bytes {
|
||||||
|
/// ### 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 Frame, area: Rect) {
|
||||||
|
self.input.render(render, 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 {
|
||||||
|
let msg: Msg = self.input.update(props);
|
||||||
|
match msg {
|
||||||
|
Msg::OnChange(Payload::One(Value::Str(input))) => {
|
||||||
|
match parse_bytesize(input.as_str()) {
|
||||||
|
Some(bytes) => {
|
||||||
|
// return OK
|
||||||
|
self.update_colors(self.native_color);
|
||||||
|
Msg::OnChange(Payload::One(Value::U64(bytes.as_u64())))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Invalid color
|
||||||
|
self.update_colors(Color::Red);
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg => 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) -> Props {
|
||||||
|
self.input.get_props()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### on
|
||||||
|
///
|
||||||
|
/// Handle input event and update internal states.
|
||||||
|
/// Returns a Msg to the view
|
||||||
|
fn on(&mut self, ev: Event) -> Msg {
|
||||||
|
// Capture message from input
|
||||||
|
match self.input.on(ev) {
|
||||||
|
Msg::OnChange(Payload::One(Value::Str(input))) => {
|
||||||
|
// Capture color and validate
|
||||||
|
match parse_bytesize(input.as_str()) {
|
||||||
|
Some(bytes) => {
|
||||||
|
// Update color and return OK
|
||||||
|
self.update_colors(self.native_color);
|
||||||
|
Msg::OnChange(Payload::One(Value::U64(bytes.as_u64())))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Invalid color
|
||||||
|
self.update_colors(Color::Red);
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::OnSubmit(_) => Msg::None,
|
||||||
|
msg => msg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_state
|
||||||
|
///
|
||||||
|
/// Get current state from component
|
||||||
|
/// For this component returns Unsigned if the input type is a number, otherwise a text
|
||||||
|
/// The value is always the current input.
|
||||||
|
fn get_state(&self) -> Payload {
|
||||||
|
match self.input.get_state() {
|
||||||
|
Payload::One(Value::Str(bytes)) => match parse_bytesize(bytes.as_str()) {
|
||||||
|
None => Payload::None,
|
||||||
|
Some(bytes) => Payload::One(Value::U64(bytes.as_u64())),
|
||||||
|
},
|
||||||
|
_ => Payload::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- events
|
||||||
|
|
||||||
|
/// ### blur
|
||||||
|
///
|
||||||
|
/// Blur component; basically remove focus
|
||||||
|
fn blur(&mut self) {
|
||||||
|
self.input.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### active
|
||||||
|
///
|
||||||
|
/// Active component; basically give focus
|
||||||
|
fn active(&mut self) {
|
||||||
|
self.input.active();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bytes_input() {
|
||||||
|
let mut component: Bytes = Bytes::new(
|
||||||
|
BytesPropsBuilder::default()
|
||||||
|
.visible()
|
||||||
|
.with_value(1024)
|
||||||
|
.with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0))
|
||||||
|
.with_label("omar", Alignment::Left)
|
||||||
|
.with_foreground(Color::Red)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
// Focus
|
||||||
|
component.blur();
|
||||||
|
component.active();
|
||||||
|
// Get value
|
||||||
|
assert_eq!(component.get_state(), Payload::One(Value::U64(1024)));
|
||||||
|
// Set an invalid color
|
||||||
|
let props = InputPropsBuilder::from(component.get_props())
|
||||||
|
.with_value(String::from("#pippo1"))
|
||||||
|
.hidden()
|
||||||
|
.build();
|
||||||
|
assert_eq!(component.update(props), Msg::None);
|
||||||
|
assert_eq!(component.get_state(), Payload::None);
|
||||||
|
// Reset color
|
||||||
|
let props = BytesPropsBuilder::from(component.get_props())
|
||||||
|
.with_value(111)
|
||||||
|
.hidden()
|
||||||
|
.build();
|
||||||
|
assert_eq!(
|
||||||
|
component.update(props),
|
||||||
|
Msg::OnChange(Payload::One(Value::U64(111)))
|
||||||
|
);
|
||||||
|
// Backspace (invalid)
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// Press '1'
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('B')))),
|
||||||
|
Msg::OnChange(Payload::One(Value::U64(111)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@
|
||||||
*/
|
*/
|
||||||
// exports
|
// exports
|
||||||
pub mod bookmark_list;
|
pub mod bookmark_list;
|
||||||
|
pub mod bytes;
|
||||||
pub mod color_picker;
|
pub mod color_picker;
|
||||||
pub mod file_list;
|
pub mod file_list;
|
||||||
pub mod logbox;
|
pub mod logbox;
|
||||||
|
|
|
@ -287,6 +287,25 @@ pub fn shadow_password(s: &str) -> String {
|
||||||
(0..s.len()).map(|_| '*').collect()
|
(0..s.len()).map(|_| '*').collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### fmt_bytes
|
||||||
|
///
|
||||||
|
/// Format bytes
|
||||||
|
pub fn fmt_bytes(v: u64) -> String {
|
||||||
|
if v >= 1125899906842624 {
|
||||||
|
format!("{} PB", v / 1125899906842624)
|
||||||
|
} else if v >= 1099511627776 {
|
||||||
|
format!("{} TB", v / 1099511627776)
|
||||||
|
} else if v >= 1073741824 {
|
||||||
|
format!("{} GB", v / 1073741824)
|
||||||
|
} else if v >= 1048576 {
|
||||||
|
format!("{} MB", v / 1048576)
|
||||||
|
} else if v >= 1024 {
|
||||||
|
format!("{} KB", v / 1024)
|
||||||
|
} else {
|
||||||
|
format!("{} B", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
@ -599,4 +618,14 @@ mod tests {
|
||||||
fn test_utils_fmt_shadow_password() {
|
fn test_utils_fmt_shadow_password() {
|
||||||
assert_eq!(shadow_password("foobar"), String::from("******"));
|
assert_eq!(shadow_password("foobar"), String::from("******"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_bytes() {
|
||||||
|
assert_eq!(fmt_bytes(110).as_str(), "110 B");
|
||||||
|
assert_eq!(fmt_bytes(2048).as_str(), "2 KB");
|
||||||
|
assert_eq!(fmt_bytes(2097152).as_str(), "2 MB");
|
||||||
|
assert_eq!(fmt_bytes(4294967296).as_str(), "4 GB");
|
||||||
|
assert_eq!(fmt_bytes(3298534883328).as_str(), "3 TB");
|
||||||
|
assert_eq!(fmt_bytes(3377699720527872).as_str(), "3 PB");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ use crate::system::config_client::ConfigClient;
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
|
|
||||||
// Ext
|
// Ext
|
||||||
|
use bytesize::ByteSize;
|
||||||
use chrono::format::ParseError;
|
use chrono::format::ParseError;
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
@ -95,6 +96,12 @@ lazy_static! {
|
||||||
* - group 6: blue
|
* - group 6: blue
|
||||||
*/
|
*/
|
||||||
static ref COLOR_RGB_REGEX: Regex = Regex::new(r"^(rgb)?\(?([01]?\d\d?|2[0-4]\d|25[0-5])(\W+)([01]?\d\d?|2[0-4]\d|25[0-5])\W+(([01]?\d\d?|2[0-4]\d|25[0-5])\)?)").unwrap();
|
static ref COLOR_RGB_REGEX: Regex = Regex::new(r"^(rgb)?\(?([01]?\d\d?|2[0-4]\d|25[0-5])(\W+)([01]?\d\d?|2[0-4]\d|25[0-5])\W+(([01]?\d\d?|2[0-4]\d|25[0-5])\)?)").unwrap();
|
||||||
|
/**
|
||||||
|
* Regex matches:
|
||||||
|
* - group 1: amount (number)
|
||||||
|
* - group 4: unit (K, M, G, T, P)
|
||||||
|
*/
|
||||||
|
static ref BYTESIZE_REGEX: Regex = Regex::new(r"(:?([0-9])+)( )*(:?[KMGTP])?B").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- remote opts
|
// -- remote opts
|
||||||
|
@ -549,6 +556,57 @@ fn parse_rgb_color(color: &str) -> Option<Color> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
enum ByteUnit {
|
||||||
|
Byte,
|
||||||
|
Kilobyte,
|
||||||
|
Megabyte,
|
||||||
|
Gigabyte,
|
||||||
|
Terabyte,
|
||||||
|
Petabyte,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ByteUnit {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"B" => Ok(Self::Byte),
|
||||||
|
"KB" => Ok(Self::Kilobyte),
|
||||||
|
"MB" => Ok(Self::Megabyte),
|
||||||
|
"GB" => Ok(Self::Gigabyte),
|
||||||
|
"TB" => Ok(Self::Terabyte),
|
||||||
|
"PB" => Ok(Self::Petabyte),
|
||||||
|
_ => Err("Invalid unit"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### parse_bytesize
|
||||||
|
///
|
||||||
|
/// Parse bytes repr (e.g. `24 MB`) into `ByteSize`
|
||||||
|
pub fn parse_bytesize<S: AsRef<str>>(bytes: S) -> Option<ByteSize> {
|
||||||
|
match BYTESIZE_REGEX.captures(bytes.as_ref()) {
|
||||||
|
None => None,
|
||||||
|
Some(groups) => {
|
||||||
|
let amount = groups
|
||||||
|
.get(1)
|
||||||
|
.map(|x| x.as_str().parse::<u64>().unwrap_or(0))?;
|
||||||
|
let unit = groups.get(4).map(|x| x.as_str().to_string());
|
||||||
|
let unit = format!("{}B", unit.unwrap_or_default());
|
||||||
|
let unit = ByteUnit::from_str(unit.as_str()).unwrap();
|
||||||
|
Some(match unit {
|
||||||
|
ByteUnit::Byte => ByteSize::b(amount),
|
||||||
|
ByteUnit::Gigabyte => ByteSize::gib(amount),
|
||||||
|
ByteUnit::Kilobyte => ByteSize::kib(amount),
|
||||||
|
ByteUnit::Megabyte => ByteSize::mib(amount),
|
||||||
|
ByteUnit::Petabyte => ByteSize::pib(amount),
|
||||||
|
ByteUnit::Terabyte => ByteSize::tib(amount),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
@ -1055,4 +1113,25 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert!(parse_color("redd").is_none());
|
assert!(parse_color("redd").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_byteunit() {
|
||||||
|
assert_eq!(ByteUnit::from_str("B").ok().unwrap(), ByteUnit::Byte);
|
||||||
|
assert_eq!(ByteUnit::from_str("KB").ok().unwrap(), ByteUnit::Kilobyte);
|
||||||
|
assert_eq!(ByteUnit::from_str("MB").ok().unwrap(), ByteUnit::Megabyte);
|
||||||
|
assert_eq!(ByteUnit::from_str("GB").ok().unwrap(), ByteUnit::Gigabyte);
|
||||||
|
assert_eq!(ByteUnit::from_str("TB").ok().unwrap(), ByteUnit::Terabyte);
|
||||||
|
assert_eq!(ByteUnit::from_str("PB").ok().unwrap(), ByteUnit::Petabyte);
|
||||||
|
assert!(ByteUnit::from_str("uB").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_str_as_bytesize() {
|
||||||
|
assert_eq!(parse_bytesize("1024 B").unwrap().as_u64(), 1024);
|
||||||
|
assert_eq!(parse_bytesize("1024B").unwrap().as_u64(), 1024);
|
||||||
|
assert_eq!(parse_bytesize("10240 KB").unwrap().as_u64(), 10485760);
|
||||||
|
assert_eq!(parse_bytesize("2 GB").unwrap().as_u64(), 2147483648);
|
||||||
|
assert_eq!(parse_bytesize("1 TB").unwrap().as_u64(), 1099511627776);
|
||||||
|
assert!(parse_bytesize("1 XB").is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue