mirror of
https://github.com/veeso/termscp.git
synced 2026-09-20 19:15:49 -07:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ad18ea5e7 | |||
| 2ed71c7ff8 | |||
| 49102985a4 | |||
| bc59df494b | |||
| 080c013fab | |||
| 6252df2959 | |||
| 9160c52cb0 | |||
| 38f1fccfd0 | |||
| 6ceaf048b5 | |||
| 534c16427e | |||
| 9b19240925 | |||
| 0ddbf3b104 | |||
| 126e58503f | |||
| 0be8b60c7d | |||
| 4ed6b118ca | |||
| a48b226d9b | |||
| e11af03fdd | |||
| f044e6ace2 | |||
| e056afbd28 | |||
| 9442e3b150 | |||
| 9c00b9a86c | |||
| 622cfe260a | |||
| 0c3ced012e | |||
| d60c5007f9 | |||
| 7e10b8e68b | |||
| 08509caf1b | |||
| 89de2d2dfa | |||
| abc80b93ad | |||
| 1b79a6fc91 | |||
| 5a3aa64fba | |||
| bc4b096077 | |||
| 0025b87b0f | |||
| bd0843d3da | |||
| 0ba43d5714 | |||
| 97e1ca789a | |||
| 2df13fac10 | |||
| 186313f57c | |||
| 8b0be49609 | |||
| e062ee0447 | |||
| c81cf39fe9 | |||
| 9cfef867a9 | |||
| 18314e6a2e | |||
| 9f3df1a79a | |||
| fe424107e9 | |||
| d97535894c | |||
| 4d65e48b3c | |||
| 0d24662b3d | |||
| bc14b049c0 | |||
| d32c588f15 | |||
| 60e86aff2a | |||
| 5f83a1468c | |||
| 5758a12c3b | |||
| 49a74d02ea | |||
| f6d35f1eea | |||
| 3fb61a76fe | |||
| ff4452c971 | |||
| fd1d8a0e26 | |||
| b0d6da8e23 | |||
| 4ca0865fe3 | |||
| b8115362f8 | |||
| 155f747563 | |||
| b04976bde3 | |||
| 5f7a0d8a46 | |||
| 694232564a | |||
| 54b674ad43 |
@@ -0,0 +1,4 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: veeso
|
||||||
|
liberapay: veeso
|
||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TERMSCP_VERSION: "0.19.0"
|
TERMSCP_VERSION: "1.0.0"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-binaries:
|
build-binaries:
|
||||||
@@ -28,14 +28,25 @@ jobs:
|
|||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
debian_suffix: amd64
|
debian_suffix: amd64
|
||||||
|
|
||||||
|
- release_for: Linux-aarch64
|
||||||
|
os: ubuntu-24.04-arm
|
||||||
|
platform: linux
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
debian_suffix: arm64
|
||||||
|
|
||||||
- release_for: Windows-x86_64
|
- release_for: Windows-x86_64
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
platform: windows
|
platform: windows
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
- release_for: Windows-aarch64
|
||||||
|
os: windows-11-arm
|
||||||
|
platform: windows
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v6
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
@@ -148,7 +159,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
name: termscp-${{ matrix.platform.target }}
|
name: termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}
|
||||||
path: target/${{ matrix.platform.target }}/release/termscp.exe
|
path: target/${{ matrix.platform.target }}/release/termscp.exe
|
||||||
|
|
||||||
- name: Upload artifact (Deb)
|
- name: Upload artifact (Deb)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: codeberg-mirror
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mirror:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: "Mirror to Codeberg"
|
||||||
|
uses: yesolutions/mirror-action@v0.7.0
|
||||||
|
with:
|
||||||
|
REMOTE: 'ssh://git@codeberg.org/veeso/termscp.git'
|
||||||
|
GIT_SSH_PRIVATE_KEY: ${{ secrets.GIT_SSH_PRIVATE_KEY }}
|
||||||
|
GIT_SSH_NO_VERIFY_HOST: "true"
|
||||||
@@ -11,13 +11,17 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-latest
|
||||||
|
- macos-latest
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v6
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y curl wget libsmbclient
|
|
||||||
- name: Install termscp from script
|
- name: Install termscp from script
|
||||||
run: |
|
run: |
|
||||||
./install.sh -v=0.12.3 -f
|
./install.sh -f
|
||||||
which termscp || exit 1
|
which termscp || exit 1
|
||||||
|
termscp --version
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ on:
|
|||||||
- "*.md"
|
- "*.md"
|
||||||
- "./site/**/*"
|
- "./site/**/*"
|
||||||
push:
|
push:
|
||||||
|
branches: [ main ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "*.md"
|
- "*.md"
|
||||||
- "./site/**/*"
|
- "./site/**/*"
|
||||||
@@ -14,13 +15,19 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v6
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
|
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- name: Format
|
||||||
|
run: cargo +nightly fmt --all -- --check
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
@@ -30,7 +37,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
args: --no-default-features --features github-actions --no-fail-fast
|
args: --no-default-features --features github-actions --no-fail-fast
|
||||||
- name: Format
|
|
||||||
run: cargo fmt --all -- --check
|
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy -- -Dwarnings
|
run: cargo clippy -- -Dwarnings
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ on:
|
|||||||
- "*.md"
|
- "*.md"
|
||||||
- "./site/**/*"
|
- "./site/**/*"
|
||||||
push:
|
push:
|
||||||
|
branches: [ main ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "*.md"
|
- "*.md"
|
||||||
- "./site/**/*"
|
- "./site/**/*"
|
||||||
@@ -14,10 +15,10 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-macos:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v6
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ on:
|
|||||||
- "*.md"
|
- "*.md"
|
||||||
- "./site/**/*"
|
- "./site/**/*"
|
||||||
push:
|
push:
|
||||||
|
branches: [ main ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "*.md"
|
- "*.md"
|
||||||
- "./site/**/*"
|
- "./site/**/*"
|
||||||
@@ -14,11 +15,11 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v6
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ dist/pkgs/arch/*.tar.gz
|
|||||||
|
|
||||||
dist/pkgs/
|
dist/pkgs/
|
||||||
dist/build/macos/openssl/
|
dist/build/macos/openssl/
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
|||||||
+218
-40
@@ -1,47 +1,225 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
- [Changelog](#changelog)
|
|
||||||
- [0.19.0](#0190)
|
|
||||||
- [0.18.0](#0180)
|
|
||||||
- [0.17.0](#0170)
|
|
||||||
- [0.16.1](#0161)
|
|
||||||
- [0.16.0](#0160)
|
|
||||||
- [0.15.0](#0150)
|
|
||||||
- [0.14.0](#0140)
|
|
||||||
- [0.13.0](#0130)
|
|
||||||
- [0.12.3](#0123)
|
|
||||||
- [0.12.2](#0122)
|
|
||||||
- [0.12.1](#0121)
|
|
||||||
- [0.12.0](#0120)
|
|
||||||
- [0.11.3](#0113)
|
|
||||||
- [0.11.2](#0112)
|
|
||||||
- [0.11.1](#0111)
|
|
||||||
- [0.11.0](#0110)
|
|
||||||
- [0.10.0](#0100)
|
|
||||||
- [0.9.0](#090)
|
|
||||||
- [0.8.2](#082)
|
|
||||||
- [0.8.1](#081)
|
|
||||||
- [0.8.0](#080)
|
|
||||||
- [0.7.0](#070)
|
|
||||||
- [0.6.1](#061)
|
|
||||||
- [0.6.0](#060)
|
|
||||||
- [0.5.1](#051)
|
|
||||||
- [0.5.0](#050)
|
|
||||||
- [0.4.2](#042)
|
|
||||||
- [0.4.1](#041)
|
|
||||||
- [0.4.0](#040)
|
|
||||||
- [0.3.3](#033)
|
|
||||||
- [0.3.2](#032)
|
|
||||||
- [0.3.1](#031)
|
|
||||||
- [0.3.0](#030)
|
|
||||||
- [0.2.0](#020)
|
|
||||||
- [0.1.3](#013)
|
|
||||||
- [0.1.2](#012)
|
|
||||||
- [0.1.1](#011)
|
|
||||||
- [0.1.0](#010)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
Released on 2026-04-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- rework TransferProgress to track bytes with lazy estimation
|
||||||
|
- replace dual progress bar components with single TransferProgressBar
|
||||||
|
- simplify progress bar layout to single component
|
||||||
|
- update progress bar display for new unified data model
|
||||||
|
- update transfer loop to use unified TransferProgress
|
||||||
|
- consolidate theme progress bar fields into single transfer_progress_bar
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
- Codeberg mirroring
|
||||||
|
- run test workflows once
|
||||||
|
- check fmt with nightly toolchain
|
||||||
|
- add linux and windows aarch64 build targets
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- FileTransferActivity pane-agnostic dispatch (#386)
|
||||||
|
> Comprehensive design for incremental refactoring of the 13k-line
|
||||||
|
> FileTransferActivity god-struct using a unified Pane abstraction.
|
||||||
|
> Detailed step-by-step plan covering 6 phases: split monoliths,
|
||||||
|
> error handling, Pane struct, action dedup, session split, view reorg.
|
||||||
|
> Extract 26 popup components from the monolithic 1,868-line popups.rs
|
||||||
|
> into 20 individual files under popups/. Each file contains one or two
|
||||||
|
> related components with their own imports. The popups.rs module file
|
||||||
|
> now contains only module declarations and re-exports.
|
||||||
|
> Replace 8 panic!() calls with error!() logging and early returns/fallthrough.
|
||||||
|
> These panics documented invariants (e.g. "this tab can't do X") but would crash
|
||||||
|
> the app if somehow triggered. Error logging is safer and more resilient.
|
||||||
|
> Replace raw FileExplorer fields in Browser with Pane structs that bundle
|
||||||
|
> the explorer and connected state. Move host_bridge_connected and
|
||||||
|
> remote_connected from FileTransferActivity into the panes. Add navigation
|
||||||
|
> API (fs_pane, opposite_pane, is_find_tab) for future unification tasks.
|
||||||
|
> Rename private get_selected_file to get_selected_file_by_id and add three
|
||||||
|
> new unified methods (get_selected_entries, get_selected_file, is_selected_one)
|
||||||
|
> that dispatch based on self.browser.tab(). Old per-tab methods are kept for
|
||||||
|
> now until their callers are migrated in subsequent tasks.
|
||||||
|
> Collapse _local_/_remote_ action method pairs (mkdir, delete, symlink,
|
||||||
|
> chmod, rename, copy) into unified methods that branch internally on
|
||||||
|
> is_local_tab(). This halves the number of action methods and simplifies
|
||||||
|
> the update.rs dispatch logic. Also unifies ShowFileInfoPopup and
|
||||||
|
> ShowChmodPopup dispatching to use get_selected_entries().
|
||||||
|
> Move `host_bridge` and `client` filesystem fields from FileTransferActivity
|
||||||
|
> into the Pane struct, enabling tab-agnostic dispatch via `fs_pane()`/
|
||||||
|
> `fs_pane_mut()`. This eliminates most `is_local_tab()` branching across
|
||||||
|
> 15+ action files.
|
||||||
|
> Key changes:
|
||||||
|
> - Add `fs: Box<dyn HostBridge>` to Pane, remove from FileTransferActivity
|
||||||
|
> - Replace per-side method pairs with unified pane-dispatched methods
|
||||||
|
> - Unify navigation (changedir, reload, scan, file_exists, has_file_changed)
|
||||||
|
> - Replace 147-line popup if/else chain with data-driven priority table
|
||||||
|
> - Replace assert!/panic!/unreachable! with proper error handling
|
||||||
|
> - Fix typo "filetransfer_activiy" across ~29 files
|
||||||
|
> - Add unit tests for Pane
|
||||||
|
>
|
||||||
|
> Net result: -473 lines, single code path for most file operations.
|
||||||
|
- replace lazy_static with std::sync::LazyLock
|
||||||
|
- migrate from mod.rs to named module files
|
||||||
|
- split parser internals into focused modules
|
||||||
|
- split auth form components by protocol
|
||||||
|
- split auth update handlers by context
|
||||||
|
- split auth view helpers by responsibility
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- date
|
||||||
|
- add core module and API documentation
|
||||||
|
- document core host and ssh modules
|
||||||
|
- document parser and file transfer params
|
||||||
|
- complete remaining core module docs
|
||||||
|
- document ssh key storage API
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- replace panics reachable from user input with proper error handling
|
||||||
|
- replace assert! calls in UI activities with graceful error handling
|
||||||
|
- correct typos in BadSytax and theme_provider log messages
|
||||||
|
- replace magic-crypt with aes-gcm for bookmark encryption
|
||||||
|
> magic-crypt has known vulnerabilities. Replace it with aes-gcm for new
|
||||||
|
> encryption (authenticated, with random nonces) while keeping a legacy
|
||||||
|
> AES-128-CBC decryption path to transparently handle existing bookmarks.
|
||||||
|
- replace recursive byte-counting with entry-based transfer progress (#395)
|
||||||
|
> * fix: replace recursive byte-counting with entry-based transfer progress
|
||||||
|
>
|
||||||
|
> Replace the expensive recursive `get_total_transfer_size` pre-calculation
|
||||||
|
> with a lightweight entry-based counter (`TransferProgress`) for the
|
||||||
|
> overall progress bar. This avoids deep `list_dir` traversals before
|
||||||
|
> transfers begin, which could cause FTP idle-timeout disconnections on
|
||||||
|
> large directory trees.
|
||||||
|
>
|
||||||
|
> The per-file byte-level progress bar (`ProgressStates`) remains
|
||||||
|
> unchanged. Bytes are still tracked via `TransferStates::add_bytes` for
|
||||||
|
> notification threshold logic.
|
||||||
|
- return after empty terminal prompt
|
||||||
|
- resolve `.` and `..` in terminal `cd` and prevent panic in path elide
|
||||||
|
> `absolutize` now lexically normalizes paths so `cd ..` navigates to the
|
||||||
|
> parent directory instead of appending `..` literally. Also guard against
|
||||||
|
> `file_name()` returning `None` in `fmt_path_elide_ex`, which caused a
|
||||||
|
> panic on paths containing unresolved `..` components.
|
||||||
|
- pass full command string to exec, not just the first word
|
||||||
|
> The `Exec` arm of `Command::from_str` only captured the first
|
||||||
|
> whitespace-delimited token, silently dropping all arguments.
|
||||||
|
> Now passes the entire input string so e.g. `ls -la /tmp` works.
|
||||||
|
- sync browsing when entering a directory from filtered/fuzzy view
|
||||||
|
- stabilize core error handling
|
||||||
|
> Remove production panic and unwrap paths from core modules.
|
||||||
|
>
|
||||||
|
> Propagate bookmark encryption failures, harden file watcher and temp mapped file handling, and clean up dead code in shared utilities.
|
||||||
|
- normalize localhost relative path checks
|
||||||
|
- use time-based redraw interval instead of progress-delta threshold
|
||||||
|
> The old 1% progress threshold caused the UI to appear frozen on large
|
||||||
|
> files (e.g. 1GB) because many read/write iterations passed between
|
||||||
|
> redraws. Switching to a 100ms time-based interval ensures consistent
|
||||||
|
> UI responsiveness regardless of file size.
|
||||||
|
- render progress bar immediately after mounting
|
||||||
|
> Call self.view() right after mount_progress_bar() at all 6 call sites
|
||||||
|
> so the bar is visible on screen before the transfer loop begins.
|
||||||
|
- filter self-references and dot entries from remote directory listings
|
||||||
|
> Some non-compliant FTP servers (e.g. LiteSpeed) include a self-reference
|
||||||
|
> to the listed directory in the LIST response, causing the current folder
|
||||||
|
> to appear as a duplicate entry in the explorer.
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
- Use apt to install termscp on debian base to prevent broken deps
|
||||||
|
- Copilot is so dumb
|
||||||
|
- Install with apt if installed
|
||||||
|
- WTF CTRL SAVE DOESNT FUCKING WORK
|
||||||
|
- funding
|
||||||
|
- add centralized lint configuration to Cargo.toml
|
||||||
|
- 1.0.0 version into manifests
|
||||||
|
- format toml
|
||||||
|
- add fmt+clippy convention to CLAUDE.md and apply nightly fmt
|
||||||
|
- enable safe clippy pedantic lints
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- use sort_by_cached_key to avoid repeated lowercase allocations in file sorting
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- add parser and bookmark regression coverage
|
||||||
|
- extend config and explorer regression coverage
|
||||||
|
- extend system regression coverage
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- removed `hostname`, use `whoami` instead
|
||||||
|
> whoami provides `hostname` function, so we don't need the hostname dependency, since whoami is also being used for getting the username
|
||||||
|
- replaced libssh with russh for remotefs-ssh
|
||||||
|
- replace `version-compare` crate with `semver`
|
||||||
|
- remotefs-ssh 0.8.1
|
||||||
|
- remotefs-ssh 0.8.2
|
||||||
|
> this version removes any usage of sh commands from the sftp backend and only uses pure protocol functions
|
||||||
|
- migrate to tui-realm 4.0
|
||||||
|
> Upgrade tuirealm (3.x -> 4.0.0), tui-realm-stdlib (3 -> 4), tui-term
|
||||||
|
> (0.2 -> 0.3). Apply all breaking changes from the 4.0 migration guide
|
||||||
|
> across the termscp UI.
|
||||||
|
>
|
||||||
|
> Key changes:
|
||||||
|
>
|
||||||
|
> - Root-level re-exports removed; imports moved to module-qualified
|
||||||
|
> paths (`tuirealm::application`, `::component`, `::event`, `::props`,
|
||||||
|
> `::state`, `::subscription`, `::listener`, `::ratatui`). Same for
|
||||||
|
> stdlib component types (`tui_realm_stdlib::components::*`).
|
||||||
|
> - `MockComponent` trait renamed to `Component`; old `Component` trait
|
||||||
|
> renamed to `AppComponent`. `#[derive(MockComponent)]` is now
|
||||||
|
> `#[derive(Component)]`. `Component::on` now takes `&Event<_>`.
|
||||||
|
> - `TextSpan` replaced with `SpanStatic`/`LineStatic`/`TextStatic`
|
||||||
|
> (ratatui-based); tuple `(String, Alignment)` titles replaced with
|
||||||
|
> the new `Title` builder; `Alignment` split into
|
||||||
|
> `HorizontalAlignment`/`VerticalAlignment`; stdlib components use
|
||||||
|
> `.alignment_horizontal` instead of `.alignment`.
|
||||||
|
> - `State::One`/`PropPayload::One` -> `Single`. `CmdResult::None`
|
||||||
|
> -> `NoChange`. `Props::get_or` removed; `Props::get` now returns a
|
||||||
|
> borrowed `Option<&AttrValue>` (call sites switched to
|
||||||
|
> `.and_then(AttrValue::as_*)`). `Component::query` returns
|
||||||
|
> `Option<QueryResult<'a>>`.
|
||||||
|
> - `Attribute::HighlightedColor` -> `HighlightStyle` (a full `Style`).
|
||||||
|
> `.highlighted_*` helpers renamed to `.highlight_*`.
|
||||||
|
> - `PollStrategy::UpTo(n)` now requires a `Duration`; tick timeout moved
|
||||||
|
> from `EventListenerCfg::poll_timeout` into `PollStrategy`.
|
||||||
|
> - `TerminalBridge` removed; `Context` now holds
|
||||||
|
> `CrosstermTerminalAdapter` directly and enables raw mode + alternate
|
||||||
|
> screen explicitly. The `TerminalAdapter` trait is imported where its
|
||||||
|
> methods are used.
|
||||||
|
> - `Update` trait removed; activity `update` methods are plain inherent
|
||||||
|
> functions.
|
||||||
|
> - `ProgressBar` replaced by stdlib `Gauge`. Paragraph `.wrap` renamed
|
||||||
|
> to `.wrap_trim`; `.text` now takes an `Into<Text>`. Stdlib `List` row
|
||||||
|
> items are now individual lines (`Vec<Span>` per row) rather than a
|
||||||
|
> `Table` of spans; custom `FileList`/`Log` convert between the two
|
||||||
|
> models.
|
||||||
|
> - Radio builders drop `.foreground(color)` so unselected items render
|
||||||
|
> with the terminal default foreground, and set
|
||||||
|
> `highlight_style(Style::default().fg(color).add_modifier(REVERSED))`
|
||||||
|
> so the selected entry is visibly highlighted only with the theme
|
||||||
|
> color.
|
||||||
|
> - Custom `FileList` keeps the selected row highlighted with the full
|
||||||
|
> highlight style when focused and falls back to a foreground-only
|
||||||
|
> style when unfocused.
|
||||||
|
> - Theme loading is now backwards compatible: `Theme` uses a custom
|
||||||
|
> `Deserialize` through an intermediate `ThemeFile` with optional
|
||||||
|
> fields, so missing keys, unknown values or legacy aliases
|
||||||
|
> (`transfer_progress_bar_full`/`_partial`) fall back to defaults on a
|
||||||
|
> per-field basis instead of failing the whole load.
|
||||||
|
- upgrade remotefs-ssh to 0.8.3
|
||||||
|
|
||||||
|
## 0.19.1
|
||||||
|
|
||||||
|
Released on 2026-04-18
|
||||||
|
|
||||||
|
- [Issue 371](https://github.com/veeso/termscp/issues/371): Updated dependencies to allow build on NetBSD
|
||||||
|
|
||||||
## 0.19.0
|
## 0.19.0
|
||||||
|
|
||||||
Released on 11/11/2025
|
Released on 11/11/2025
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
termscp is a terminal file transfer client with a TUI (Terminal User Interface), supporting SFTP, SCP, FTP/FTPS, Kube, S3, SMB, and WebDAV protocols. It features a dual-pane file explorer (local + remote), bookmarks, system keyring integration, file watching/sync, an embedded terminal, and customizable themes.
|
||||||
|
|
||||||
|
- **Language**: Rust (edition 2024, MSRV 1.89.0)
|
||||||
|
- **UI Framework**: tuirealm v3 (built on crossterm)
|
||||||
|
- **File Transfer**: remotefs ecosystem
|
||||||
|
|
||||||
|
## Build & Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
cargo build
|
||||||
|
cargo build --release
|
||||||
|
cargo build --no-default-features # minimal build without SMB/keyring
|
||||||
|
|
||||||
|
# Test (CI-equivalent)
|
||||||
|
cargo test --no-default-features --features github-actions --no-fail-fast
|
||||||
|
|
||||||
|
# Run a single test
|
||||||
|
cargo test <test_name> -- --nocapture
|
||||||
|
|
||||||
|
# Run tests for a module
|
||||||
|
cargo test --lib filetransfer::
|
||||||
|
cargo test --lib config::params::tests
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
cargo clippy -- -Dwarnings
|
||||||
|
|
||||||
|
# Format
|
||||||
|
cargo fmt --all -- --check # check only
|
||||||
|
cargo fmt --all # fix
|
||||||
|
```
|
||||||
|
|
||||||
|
### System Dependencies (for building)
|
||||||
|
|
||||||
|
- **Linux**: `libdbus-1-dev`, `libsmbclient-dev`
|
||||||
|
- **macOS**: `pkg-config`, `samba` (brew, with force link)
|
||||||
|
|
||||||
|
## Feature Flags
|
||||||
|
|
||||||
|
- **`smb`** (default): SMB/Samba protocol support
|
||||||
|
- **`keyring`** (default): System keyring integration for password storage
|
||||||
|
- **`smb-vendored`**: Vendored SMB library (for static builds)
|
||||||
|
- **`github-actions`**: CI flag — disables real keyring in tests, uses file-based storage
|
||||||
|
- **`isolated-tests`**: For parallel test isolation
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Application Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
main.rs → parse CLI args → ActivityManager::new() → ActivityManager::run()
|
||||||
|
↓
|
||||||
|
Activity loop (draw → poll → update)
|
||||||
|
├── AuthActivity (login/bookmarks)
|
||||||
|
├── FileTransferActivity (dual-pane explorer)
|
||||||
|
└── SetupActivity (configuration)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ActivityManager` owns a `Context` that is passed between activities. Each activity takes ownership of the Context on `on_create()` and returns it on `on_destroy()`.
|
||||||
|
|
||||||
|
### Key Modules
|
||||||
|
|
||||||
|
| Module | Path | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| **activity_manager** | `src/activity_manager.rs` | Orchestrates activity lifecycle and transitions |
|
||||||
|
| **ui/activities** | `src/ui/activities/{auth,filetransfer,setup}/` | Three main screens, each implementing the `Activity` trait |
|
||||||
|
| **ui/context** | `src/ui/context.rs` | Shared `Context` struct (terminal, config, bookmarks, theme) |
|
||||||
|
| **filetransfer** | `src/filetransfer/` | Protocol enum, `RemoteFsBuilder`, connection parameters |
|
||||||
|
| **host** | `src/host/` | `HostBridge` trait — abstracts local (`Localhost`) and remote (`RemoteBridged`) file operations |
|
||||||
|
| **explorer** | `src/explorer/` | `FileExplorer` — directory navigation, sorting, filtering, transfer queue |
|
||||||
|
| **system** | `src/system/` | `BookmarksClient`, `ConfigClient`, `ThemeProvider`, `SshKeyStorage`, `KeyStorage` trait |
|
||||||
|
| **config** | `src/config/` | TOML-based serialization for themes, bookmarks, user params |
|
||||||
|
|
||||||
|
### Core Traits
|
||||||
|
|
||||||
|
- **`Activity`** (`src/ui/activities/mod.rs`): `on_create`, `on_draw`, `will_umount`, `on_destroy` — UI screen lifecycle
|
||||||
|
- **`HostBridge`** (`src/host/bridge.rs`): Unified file operations interface (connect, list_dir, open_file, mkdir, remove, rename, copy, etc.)
|
||||||
|
- **`KeyStorage`** (`src/system/keys/mod.rs`): `get_key`/`set_key` — password storage abstraction (keyring or encrypted file fallback)
|
||||||
|
|
||||||
|
### Conditional Compilation
|
||||||
|
|
||||||
|
The `build.rs` defines cfg aliases via `cfg_aliases`:
|
||||||
|
- `posix`, `macos`, `linux`, `win` — platform shortcuts
|
||||||
|
- `smb`, `smb_unix`, `smb_windows` — feature + platform combinations
|
||||||
|
|
||||||
|
Platform-specific dependencies: SSH and FTP crates use different TLS backends on Unix vs Windows. SMB support is completely gated behind the `smb` feature flag.
|
||||||
|
|
||||||
|
### File Transfer Protocols
|
||||||
|
|
||||||
|
`FileTransferProtocol` enum maps to protocol-specific parameter types (`ProtocolParams` enum) and `RemoteFsBuilder` constructs the appropriate `RemoteFs` client. Each protocol has its own params struct (e.g., `GenericProtocolParams` for SSH-based, `AwsS3Params`, `KubeProtocolParams`, `SmbParams`, `WebDAVProtocolParams`).
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
|
- **rustfmt**: `group_imports = "StdExternalCrate"`, `imports_granularity = "Module"`
|
||||||
|
- **Error handling**: Custom error types with `thiserror`, module-level Result aliases (e.g., `HostResult<T>`)
|
||||||
|
- **Builder pattern**: Used for `RemoteFsBuilder`, `HostBridgeBuilder`
|
||||||
|
- **Client pattern**: System services wrapped as clients (`BookmarksClient`, `ConfigClient`)
|
||||||
|
- **Tests**: Unit tests in `#[cfg(test)]` blocks within source files. Tests requiring serial execution use `#[serial]` from `serial_test`
|
||||||
|
- **Encryption**: Bookmark passwords encrypted with `magic-crypt`; keys stored in system keyring or encrypted file
|
||||||
|
|
||||||
|
## Other conventions
|
||||||
|
|
||||||
|
- Always run `cargo +nightly fmt --all` and `cargo clippy --no-default-features -- -Dwarnings` after modifying Rust code
|
||||||
|
- Always put plans to `./.claude/plans/`
|
||||||
+1
-1
@@ -138,7 +138,7 @@ Let's make it simple and clear:
|
|||||||
In addition to the process described for the PRs, I've also decided to introduce a list of guidelines to follow when writing the code, that should be followed:
|
In addition to the process described for the PRs, I've also decided to introduce a list of guidelines to follow when writing the code, that should be followed:
|
||||||
|
|
||||||
1. **Let's stop the NPM apocalypse**: personally I'm against the abuse of dependencies we make in software projects and I think that NodeJS has opened the way to this drama (and has already gone too far). Nowadays nobody cares about adding hundreds of dependencies to their projects. Don't misunderstand me: I think that package managers are cool, but I'm totally against the abuse we're making of them. I think when we work on a project, we should try to use the minor quantity of dependencies as possible, especially because it's not hard to see how many libraries are getting abandoned right now, causing compatibility issues after a while. So please, when working on termscp, try not to add useless dependencies.
|
1. **Let's stop the NPM apocalypse**: personally I'm against the abuse of dependencies we make in software projects and I think that NodeJS has opened the way to this drama (and has already gone too far). Nowadays nobody cares about adding hundreds of dependencies to their projects. Don't misunderstand me: I think that package managers are cool, but I'm totally against the abuse we're making of them. I think when we work on a project, we should try to use the minor quantity of dependencies as possible, especially because it's not hard to see how many libraries are getting abandoned right now, causing compatibility issues after a while. So please, when working on termscp, try not to add useless dependencies.
|
||||||
2. **No C-bindings**: personally I think that Rust still relies too much on C. And that's bad, really bad. Many libraries in Rust are just wrappers to C libraries, which is a huge problem, especially considering this is a multiplatform project. Everytime you add a C-binding to your project, you're forcing your users to install additional libraries to their systems. Sometimes these libraries are already installed on their systems (as happens for libssh2 or openssl in this case), but sometimes not. So if you really have to add a dependency to this project, please AVOID completely adding C-bounded libraries.
|
2. **No C-bindings**: personally I think that Rust still relies too much on C. And that's bad, really bad. Many libraries in Rust are just wrappers to C libraries, which is a huge problem, especially considering this is a multiplatform project. Everytime you add a C-binding to your project, you're forcing your users to install additional libraries to their systems. Sometimes these libraries are already installed on their systems (as happens for openssl in this case), but sometimes not. So if you really have to add a dependency to this project, please AVOID completely adding C-bounded libraries.
|
||||||
3. **Test units matter**: Whenever you implement something new to this project, always implement test units which cover the most cases as possible.
|
3. **Test units matter**: Whenever you implement something new to this project, always implement test units which cover the most cases as possible.
|
||||||
4. **Comments are useful**: Many people say that the code should be that simple to talk by itself about what it does, and comments should then be useless. I personally don't agree. I'm not saying they're wrong, but I'm just saying that this approach has, in my personal opinion, many aspects which are underrated:
|
4. **Comments are useful**: Many people say that the code should be that simple to talk by itself about what it does, and comments should then be useless. I personally don't agree. I'm not saying they're wrong, but I'm just saying that this approach has, in my personal opinion, many aspects which are underrated:
|
||||||
1. What's obvious for me, might not be for the others.
|
1. What's obvious for me, might not be for the others.
|
||||||
|
|||||||
Generated
+2455
-960
File diff suppressed because it is too large
Load Diff
+106
-91
@@ -1,17 +1,17 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Christian Visintin <christian.visintin@veeso.dev>"]
|
name = "termscp"
|
||||||
categories = ["command-line-utilities"]
|
version = "1.0.0"
|
||||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV"
|
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
authors = ["Christian Visintin <christian.visintin@veeso.dev>"]
|
||||||
|
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/veeso/termscp"
|
||||||
|
categories = ["command-line-utilities"]
|
||||||
homepage = "https://termscp.veeso.dev"
|
homepage = "https://termscp.veeso.dev"
|
||||||
include = ["src/**/*", "build.rs", "LICENSE", "README.md", "CHANGELOG.md"]
|
include = ["src/**/*", "build.rs", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||||
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
|
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
|
||||||
license = "MIT"
|
|
||||||
name = "termscp"
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/veeso/termscp"
|
rust-version = "1.89.0"
|
||||||
version = "0.19.0"
|
|
||||||
rust-version = "1.88.0"
|
|
||||||
|
|
||||||
[package.metadata.rpm]
|
[package.metadata.rpm]
|
||||||
package = "termscp"
|
package = "termscp"
|
||||||
@@ -27,96 +27,111 @@ maintainer = "Christian Visintin <christian.visintin@veeso.dev>"
|
|||||||
copyright = "2025, Christian Visintin <christian.visintin@veeso.dev>"
|
copyright = "2025, Christian Visintin <christian.visintin@veeso.dev>"
|
||||||
extended-description-file = "docs/misc/README.deb.txt"
|
extended-description-file = "docs/misc/README.deb.txt"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "termscp"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
argh = "^0.1"
|
|
||||||
bitflags = "^2"
|
|
||||||
bytesize = "^2"
|
|
||||||
chrono = "^0.4"
|
|
||||||
content_inspector = "^0.2"
|
|
||||||
dirs = "^6"
|
|
||||||
edit = "^0.1"
|
|
||||||
filetime = "^0.2"
|
|
||||||
hostname = "^0.4"
|
|
||||||
keyring = { version = "^3", features = [
|
|
||||||
"apple-native",
|
|
||||||
"windows-native",
|
|
||||||
"sync-secret-service",
|
|
||||||
"vendored",
|
|
||||||
] }
|
|
||||||
lazy-regex = "^3"
|
|
||||||
lazy_static = "^1"
|
|
||||||
log = "^0.4"
|
|
||||||
magic-crypt = "4"
|
|
||||||
notify = "8"
|
|
||||||
notify-rust = { version = "^4", default-features = false, features = ["d"] }
|
|
||||||
nucleo = "0.5"
|
|
||||||
open = "^5.0"
|
|
||||||
rand = "^0.9"
|
|
||||||
regex = "^1"
|
|
||||||
remotefs = "^0.3"
|
|
||||||
remotefs-aws-s3 = "0.4"
|
|
||||||
remotefs-kube = "0.4"
|
|
||||||
remotefs-smb = { version = "^0.3", optional = true }
|
|
||||||
remotefs-webdav = "^0.2"
|
|
||||||
rpassword = "^7"
|
|
||||||
self_update = { version = "^0.42", default-features = false, features = [
|
|
||||||
"rustls",
|
|
||||||
"archive-tar",
|
|
||||||
"archive-zip",
|
|
||||||
"compression-flate2",
|
|
||||||
"compression-zip-deflate",
|
|
||||||
] }
|
|
||||||
serde = { version = "^1", features = ["derive"] }
|
|
||||||
shellexpand = "3"
|
|
||||||
simplelog = "^0.12"
|
|
||||||
ssh2-config = "^0.6"
|
|
||||||
tempfile = "3"
|
|
||||||
thiserror = "2"
|
|
||||||
tokio = { version = "1", features = ["rt"] }
|
|
||||||
toml = "^0.9"
|
|
||||||
tui-realm-stdlib = "3"
|
|
||||||
tuirealm = "3"
|
|
||||||
tui-term = "0.2"
|
|
||||||
unicode-width = "^0.2"
|
|
||||||
version-compare = "^0.2"
|
|
||||||
whoami = "^1.6"
|
|
||||||
wildmatch = "^2"
|
|
||||||
|
|
||||||
[target."cfg(target_family = \"unix\")".dependencies]
|
|
||||||
remotefs-ftp = { version = "^0.3", features = [
|
|
||||||
"native-tls-vendored",
|
|
||||||
"native-tls",
|
|
||||||
] }
|
|
||||||
remotefs-ssh = { version = "^0.7", default-features = false, features = [
|
|
||||||
"find",
|
|
||||||
"libssh-vendored",
|
|
||||||
] }
|
|
||||||
uzers = "0.12"
|
|
||||||
|
|
||||||
[target."cfg(target_family = \"windows\")".dependencies]
|
|
||||||
remotefs-ftp = { version = "^0.3", features = ["native-tls"] }
|
|
||||||
remotefs-ssh = { version = "^0.7" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
pretty_assertions = "^1"
|
|
||||||
serial_test = "^3"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
cfg_aliases = "0.2"
|
|
||||||
vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["smb", "keyring"]
|
default = ["keyring", "smb"]
|
||||||
github-actions = []
|
github-actions = []
|
||||||
isolated-tests = []
|
isolated-tests = []
|
||||||
keyring = []
|
keyring = []
|
||||||
smb = ["dep:remotefs-smb"]
|
smb = ["dep:remotefs-smb"]
|
||||||
smb-vendored = ["remotefs-smb/vendored"]
|
smb-vendored = ["remotefs-smb/vendored"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aes = "0.8"
|
||||||
|
aes-gcm = "0.10"
|
||||||
|
argh = "0.1"
|
||||||
|
base64 = "0.22"
|
||||||
|
bitflags = "2"
|
||||||
|
bytesize = "2"
|
||||||
|
cbc = { version = "0.1", features = ["alloc"] }
|
||||||
|
chrono = "0.4"
|
||||||
|
content_inspector = "0.2"
|
||||||
|
dirs = "6"
|
||||||
|
edit = "0.1"
|
||||||
|
filetime = "0.2"
|
||||||
|
keyring = { version = "3", features = [
|
||||||
|
"apple-native",
|
||||||
|
"sync-secret-service",
|
||||||
|
"vendored",
|
||||||
|
"windows-native",
|
||||||
|
] }
|
||||||
|
lazy-regex = "3"
|
||||||
|
log = "0.4"
|
||||||
|
md-5 = "0.10"
|
||||||
|
notify = "8"
|
||||||
|
notify-rust = { version = "4", default-features = false, features = ["d"] }
|
||||||
|
nucleo = "0.5"
|
||||||
|
open = "5"
|
||||||
|
rand = "0.9"
|
||||||
|
regex = "1"
|
||||||
|
remotefs = "0.3"
|
||||||
|
remotefs-aws-s3 = "0.4"
|
||||||
|
remotefs-kube = "0.4"
|
||||||
|
remotefs-smb = { version = "0.3", optional = true }
|
||||||
|
remotefs-ssh = { version = "0.8", default-features = false, features = ["russh"] }
|
||||||
|
remotefs-webdav = "0.2"
|
||||||
|
rpassword = "7"
|
||||||
|
self_update = { version = "0.42", default-features = false, features = [
|
||||||
|
"archive-tar",
|
||||||
|
"archive-zip",
|
||||||
|
"compression-flate2",
|
||||||
|
"compression-zip-deflate",
|
||||||
|
"rustls",
|
||||||
|
] }
|
||||||
|
semver = "1"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
shellexpand = "3"
|
||||||
|
simplelog = "0.12"
|
||||||
|
ssh2-config = "0.7"
|
||||||
|
tempfile = "3"
|
||||||
|
thiserror = "2"
|
||||||
|
tokio = { version = "1", features = ["rt"] }
|
||||||
|
toml = "1"
|
||||||
|
tui-realm-stdlib = "4"
|
||||||
|
tui-term = "0.3"
|
||||||
|
tuirealm = "4"
|
||||||
|
unicode-width = "0.2"
|
||||||
|
whoami = "2"
|
||||||
|
wildmatch = "2"
|
||||||
|
|
||||||
|
[target."cfg(target_family = \"unix\")".dependencies]
|
||||||
|
remotefs-ftp = { version = "0.4", features = [
|
||||||
|
"native-tls",
|
||||||
|
"native-tls-vendored",
|
||||||
|
] }
|
||||||
|
uzers = "0.12"
|
||||||
|
|
||||||
|
[target."cfg(target_family = \"windows\")".dependencies]
|
||||||
|
remotefs-ftp = { version = "0.4", features = ["native-tls"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "1"
|
||||||
|
serial_test = "3"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cfg_aliases = "0.2"
|
||||||
|
vergen-git2 = { version = "9", features = ["build", "cargo", "rustc", "si"] }
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "termscp"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
trivial_numeric_casts = "warn"
|
||||||
|
unsafe_op_in_unsafe_fn = "warn"
|
||||||
|
unused_lifetimes = "warn"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
complexity = { level = "warn", priority = -1 }
|
||||||
|
correctness = { level = "warn", priority = -1 }
|
||||||
|
cloned_instead_of_copied = "warn"
|
||||||
|
implicit_clone = "warn"
|
||||||
|
manual_string_new = "warn"
|
||||||
|
perf = { level = "warn", priority = -1 }
|
||||||
|
redundant_closure_for_method_calls = "warn"
|
||||||
|
style = { level = "warn", priority = -1 }
|
||||||
|
suspicious = { level = "warn", priority = -1 }
|
||||||
|
unnested_or_patterns = "warn"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
incremental = true
|
incremental = true
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Developed by <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Developed by <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Current version: 0.19.0 11/11/2025</p>
|
<p align="center">Current version: 1.0.0 2026-04-18</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
[changelog]
|
||||||
|
body = """
|
||||||
|
## {{ version | trim_start_matches(pat="v") }}
|
||||||
|
|
||||||
|
Released on {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{%- if commits | filter(attribute="breaking", value=true) | length > 0 %}
|
||||||
|
|
||||||
|
### ⚠ Breaking Changes
|
||||||
|
|
||||||
|
{%- for commit in commits | filter(attribute="breaking", value=true) %}
|
||||||
|
- {% if commit.scope %}**{{ commit.scope }}:** {% endif %}{{ commit.message | split(pat="\n") | first | trim }}
|
||||||
|
{%- if commit.breaking_description %}
|
||||||
|
> {{ commit.breaking_description }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
|
||||||
|
### {{ group | upper_first }}
|
||||||
|
|
||||||
|
{%- for commit in commits %}
|
||||||
|
- {% if commit.breaking %}💥 {% endif %}{% if commit.scope %}**{{ commit.scope }}:** {% endif %}{{ commit.message | split(pat="\n") | first | trim }}
|
||||||
|
{%- if commit.body %}
|
||||||
|
> {{ commit.body | split(pat="\n") | join(sep="\n > ") }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endfor %}
|
||||||
|
"""
|
||||||
|
trim = false
|
||||||
|
|
||||||
|
[git]
|
||||||
|
conventional_commits = true
|
||||||
|
filter_unconventional = true
|
||||||
|
split_commits = false
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "Added" },
|
||||||
|
{ message = "^fix", group = "Fixed" },
|
||||||
|
{ message = "^refactor", group = "Changed" },
|
||||||
|
{ message = "^perf", group = "Performance" },
|
||||||
|
{ message = "^doc", group = "Documentation" },
|
||||||
|
{ message = "^test", group = "Testing" },
|
||||||
|
{ message = "^ci", group = "CI" },
|
||||||
|
{ message = "^chore", group = "Miscellaneous" },
|
||||||
|
]
|
||||||
|
filter_commits = false
|
||||||
|
tag_pattern = "v[0-9].*"
|
||||||
|
sort_commits = "oldest"
|
||||||
Vendored
-1
@@ -43,7 +43,6 @@ www: \"https://termscp.veeso.dev/termscp/\"\n\
|
|||||||
maintainer: \"christian.visintin1997@gmail.com\"\n\
|
maintainer: \"christian.visintin1997@gmail.com\"\n\
|
||||||
prefix: \"/usr/local/bin\"\n\
|
prefix: \"/usr/local/bin\"\n\
|
||||||
deps: {\n\
|
deps: {\n\
|
||||||
libssh: {origin: security/libssh, version: 0.9.5}\n\
|
|
||||||
}\n\
|
}\n\
|
||||||
files: {\n\
|
files: {\n\
|
||||||
/usr/local/bin/termscp: \"$HASH\"\n\
|
/usr/local/bin/termscp: \"$HASH\"\n\
|
||||||
|
|||||||
+2
-4
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Entwickelt von <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Entwickelt von <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Aktuelle Version: 0.19.0 11/11/2025</p>
|
<p align="center">Aktuelle Version: 1.0.0 2026-04-18</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
@@ -193,12 +193,10 @@ Für weitere Informationen oder andere Plattformen besuchen Sie bitte [termscp.v
|
|||||||
### Softwareanforderungen ❗
|
### Softwareanforderungen ❗
|
||||||
|
|
||||||
- **Linux** Benutzer:
|
- **Linux** Benutzer:
|
||||||
- libssh
|
|
||||||
- libdbus-1
|
- libdbus-1
|
||||||
- pkg-config
|
- pkg-config
|
||||||
- libsmbclient
|
- libsmbclient
|
||||||
- **FreeBSD** Benutzer:
|
- **FreeBSD** Benutzer:
|
||||||
- libssh
|
|
||||||
- dbus
|
- dbus
|
||||||
- pkgconf
|
- pkgconf
|
||||||
- libsmbclient
|
- libsmbclient
|
||||||
@@ -265,7 +263,7 @@ termscp wird von diesen großartigen Projekten unterstützt:
|
|||||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||||
- [rust-s3](https://github.com/durch/rust-s3)
|
- [rust-s3](https://github.com/durch/rust-s3)
|
||||||
- [self_update](https://github.com/jaemk/self_update)
|
- [self_update](https://github.com/jaemk/self_update)
|
||||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
- [russh](https://github.com/Eugeny/russh)
|
||||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||||
|
|||||||
+2
-2
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Desarrollado por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Desarrollado por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Versión actual: 0.19.0 11/11/2025</p>
|
<p align="center">Versión actual: 1.0.0 2026-04-18</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
@@ -263,7 +263,7 @@ termscp funciona con estos increíbles proyectos:
|
|||||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||||
- [rust-s3](https://github.com/durch/rust-s3)
|
- [rust-s3](https://github.com/durch/rust-s3)
|
||||||
- [self_update](https://github.com/jaemk/self_update)
|
- [self_update](https://github.com/jaemk/self_update)
|
||||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
- [russh](https://github.com/Eugeny/russh)
|
||||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||||
|
|||||||
+2
-2
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Développé par <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Développé par <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Version actuelle: 0.19.0 11/11/2025</p>
|
<p align="center">Version actuelle: 1.0.0 2026-04-18</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
@@ -265,7 +265,7 @@ termscp est soutenu par ces projets impressionnants:
|
|||||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||||
- [rust-s3](https://github.com/durch/rust-s3)
|
- [rust-s3](https://github.com/durch/rust-s3)
|
||||||
- [self_update](https://github.com/jaemk/self_update)
|
- [self_update](https://github.com/jaemk/self_update)
|
||||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
- [russh](https://github.com/Eugeny/russh)
|
||||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||||
|
|||||||
+2
-2
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Sviluppato da <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Sviluppato da <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Versione corrente: 0.19.0 11/11/2025</p>
|
<p align="center">Versione corrente: 1.0.0 2026-04-18</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
@@ -263,7 +263,7 @@ se termscp esiste, è anche grazie a questi fantastici progetti:
|
|||||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||||
- [rust-s3](https://github.com/durch/rust-s3)
|
- [rust-s3](https://github.com/durch/rust-s3)
|
||||||
- [self_update](https://github.com/jaemk/self_update)
|
- [self_update](https://github.com/jaemk/self_update)
|
||||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
- [russh](https://github.com/Eugeny/russh)
|
||||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Desenvolvido por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
<p align="center">Desenvolvido por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Versão atual: 0.19.0 11/11/2025</p>
|
<p align="center">Versão atual: 1.0.0 2026-04-18</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">由 <a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
|
<p align="center">由 <a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
|
||||||
<p align="center">当前版本: 0.19.0 11/11/2025</p>
|
<p align="center">当前版本: 1.0.0 2026-04-18</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
@@ -196,12 +196,10 @@ choco install termscp
|
|||||||
### 依赖 ❗
|
### 依赖 ❗
|
||||||
|
|
||||||
- **Linux** 用户:
|
- **Linux** 用户:
|
||||||
- libssh
|
|
||||||
- libdbus-1
|
- libdbus-1
|
||||||
- pkg-config
|
- pkg-config
|
||||||
- libsmbclient
|
- libsmbclient
|
||||||
- **FreeBSD** 用户:
|
- **FreeBSD** 用户:
|
||||||
- libssh
|
|
||||||
- dbus
|
- dbus
|
||||||
- pkgconf
|
- pkgconf
|
||||||
- libsmbclient
|
- libsmbclient
|
||||||
@@ -270,7 +268,7 @@ termscp 由这些很棒的项目提供支持:
|
|||||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||||
- [rust-s3](https://github.com/durch/rust-s3)
|
- [rust-s3](https://github.com/durch/rust-s3)
|
||||||
- [self_update](https://github.com/jaemk/self_update)
|
- [self_update](https://github.com/jaemk/self_update)
|
||||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
- [russh](https://github.com/Eugeny/russh)
|
||||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||||
|
|||||||
+52
-30
@@ -8,10 +8,10 @@
|
|||||||
# -f, -y, --force, --yes
|
# -f, -y, --force, --yes
|
||||||
# Skip the confirmation prompt during installation
|
# Skip the confirmation prompt during installation
|
||||||
|
|
||||||
TERMSCP_VERSION="0.19.0"
|
TERMSCP_VERSION="1.0.0"
|
||||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_amd64.deb"
|
||||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_arm64.deb"
|
||||||
|
|
||||||
PATH="$PATH:/usr/sbin"
|
PATH="$PATH:/usr/sbin"
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ NO_COLOR="$(tput sgr0 2>/dev/null || printf '')"
|
|||||||
set_termscp_version() {
|
set_termscp_version() {
|
||||||
TERMSCP_VERSION="$1"
|
TERMSCP_VERSION="$1"
|
||||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_amd64.deb"
|
||||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_arm64.deb"
|
||||||
}
|
}
|
||||||
|
|
||||||
info() {
|
info() {
|
||||||
@@ -217,6 +217,49 @@ install_with_brew() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install_on_debian() {
|
||||||
|
local pkg_manager
|
||||||
|
if has apt; then
|
||||||
|
pkg_manager="apt"
|
||||||
|
elif has "apt-get"; then
|
||||||
|
pkg_manager="apt-get"
|
||||||
|
else
|
||||||
|
pkg_manager="dpkg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Detected $pkg_manager on your system"
|
||||||
|
case "${ARCH}" in
|
||||||
|
x86_64) DEB_URL="$DEB_URL_AMD64" ;;
|
||||||
|
aarch64) DEB_URL="$DEB_URL_AARCH64" ;;
|
||||||
|
*) try_with_cargo "we don't distribute packages for ${ARCH} at the moment" && return $? ;;
|
||||||
|
esac
|
||||||
|
info "Installing ${GREEN}termscp${NO_COLOR} via Debian package"
|
||||||
|
archive=$(get_tmpfile "deb")
|
||||||
|
download "${archive}" "${DEB_URL}"
|
||||||
|
info "Downloaded debian package to ${archive}"
|
||||||
|
if test_writeable "/usr/bin"; then
|
||||||
|
sudo=""
|
||||||
|
msg="Installing ${GREEN}termscp${NO_COLOR}, please wait…"
|
||||||
|
else
|
||||||
|
warn "Root permissions are required to install ${GREEN}termscp${NO_COLOR}…"
|
||||||
|
elevate_priv
|
||||||
|
sudo="sudo"
|
||||||
|
msg="Installing ${GREEN}termscp${NO_COLOR} as root, please wait…"
|
||||||
|
fi
|
||||||
|
info "$msg"
|
||||||
|
|
||||||
|
if [ "$pkg_manager" = "apt" ]; then
|
||||||
|
$sudo apt install -y "${archive}"
|
||||||
|
elif [ "$pkg_manager" = "apt-get" ]; then
|
||||||
|
$sudo dpkg -i "${archive}"
|
||||||
|
$sudo apt-get -f install
|
||||||
|
else
|
||||||
|
$sudo dpkg -i "${archive}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f ${archive}
|
||||||
|
}
|
||||||
|
|
||||||
install_on_linux() {
|
install_on_linux() {
|
||||||
local msg
|
local msg
|
||||||
local sudo
|
local sudo
|
||||||
@@ -236,28 +279,7 @@ install_on_linux() {
|
|||||||
elif has pikaur; then
|
elif has pikaur; then
|
||||||
install_on_arch_linux pikaur
|
install_on_arch_linux pikaur
|
||||||
elif has dpkg; then
|
elif has dpkg; then
|
||||||
case "${ARCH}" in
|
install_on_debian
|
||||||
x86_64) DEB_URL="$DEB_URL_AMD64" ;;
|
|
||||||
aarch64) DEB_URL="$DEB_URL_AARCH64" ;;
|
|
||||||
*) try_with_cargo "we don't distribute packages for ${ARCH} at the moment" && return $? ;;
|
|
||||||
esac
|
|
||||||
info "Detected dpkg on your system"
|
|
||||||
info "Installing ${GREEN}termscp${NO_COLOR} via Debian package"
|
|
||||||
archive=$(get_tmpfile "deb")
|
|
||||||
download "${archive}" "${DEB_URL}"
|
|
||||||
info "Downloaded debian package to ${archive}"
|
|
||||||
if test_writeable "/usr/bin"; then
|
|
||||||
sudo=""
|
|
||||||
msg="Installing ${GREEN}termscp${NO_COLOR}, please wait…"
|
|
||||||
else
|
|
||||||
warn "Root permissions are required to install ${GREEN}termscp${NO_COLOR}…"
|
|
||||||
elevate_priv
|
|
||||||
sudo="sudo"
|
|
||||||
msg="Installing ${GREEN}termscp${NO_COLOR} as root, please wait…"
|
|
||||||
fi
|
|
||||||
info "$msg"
|
|
||||||
$sudo dpkg -i "${archive}"
|
|
||||||
rm -f ${archive}
|
|
||||||
elif has brew; then
|
elif has brew; then
|
||||||
install_with_brew
|
install_with_brew
|
||||||
else
|
else
|
||||||
@@ -277,9 +299,9 @@ install_on_macos() {
|
|||||||
|
|
||||||
install_bsd_cargo_deps() {
|
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}gcc${NO_COLOR} is 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 dbus pkgconf libsmbclient
|
$sudo pkg install -y curl wget gcc dbus pkgconf libsmbclient
|
||||||
info "Dependencies installed successfully"
|
info "Dependencies installed successfully"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +327,7 @@ install_linux_cargo_deps() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
set -e
|
set -e
|
||||||
confirm "${YELLOW}libssh, gcc, openssl, pkg-config, libdbus${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}. The following command will be used to install the dependencies: '${BOLD}${YELLOW}${deps_cmd}${NO_COLOR}'. Would you like to proceed?"
|
confirm "${YELLOW}gcc, openssl, pkg-config, libdbus${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}. The following command will be used to install the dependencies: '${BOLD}${YELLOW}${deps_cmd}${NO_COLOR}'. Would you like to proceed?"
|
||||||
sudo="$(elevate_priv_ex /usr/local/bin)"
|
sudo="$(elevate_priv_ex /usr/local/bin)"
|
||||||
$sudo $deps_cmd
|
$sudo $deps_cmd
|
||||||
info "Dependencies installed successfully"
|
info "Dependencies installed successfully"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
||||||
since last release, so if the latest version is not available yet,
|
since last release, so if the latest version is not available yet,
|
||||||
you can install it downloading the ZIP file from</span>
|
you can install it downloading the ZIP file from</span>
|
||||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.19.0.nupkg"
|
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.1.0.0.nupkg"
|
||||||
target="_blank">Github</a>
|
target="_blank">Github</a>
|
||||||
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
On Debian based distros, you can install termscp using the Deb
|
On Debian based distros, you can install termscp using the Deb
|
||||||
package via:
|
package via:
|
||||||
</p>
|
</p>
|
||||||
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.19.0_amd64.deb</span>
|
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_1.0.0_amd64.deb</span>
|
||||||
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
|
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
|
||||||
</div>
|
</div>
|
||||||
<h3>
|
<h3>
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
<span translate="intro.versionAlert">termscp 0.19.0 is NOW out! Download it from</span>
|
<span translate="intro.versionAlert">termscp 1.0.0 is NOW out! Download it from</span>
|
||||||
<a href="/get-started.html" translate="intro.here">here!</a>
|
<a href="/get-started.html" translate="intro.here">here!</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "Get started →",
|
"getStarted": "Get started →",
|
||||||
"versionAlert": "termscp 0.19.0 is NOW out! Download it from",
|
"versionAlert": "termscp 1.0.0 is NOW out! Download it from",
|
||||||
"here": "here",
|
"here": "here",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "Para iniciar →",
|
"getStarted": "Para iniciar →",
|
||||||
"versionAlert": "termscp 0.19.0 ya está disponible! Descárgalo desde",
|
"versionAlert": "termscp 1.0.0 ya está disponible! Descárgalo desde",
|
||||||
"here": "aquì",
|
"here": "aquì",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "Pour commencer →",
|
"getStarted": "Pour commencer →",
|
||||||
"versionAlert": "termscp 0.19.0 est maintenant sorti! Télécharge-le depuis",
|
"versionAlert": "termscp 1.0.0 est maintenant sorti! Télécharge-le depuis",
|
||||||
"here": "ici",
|
"here": "ici",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
||||||
"getStarted": "Installa termscp →",
|
"getStarted": "Installa termscp →",
|
||||||
"versionAlert": "termscp 0.19.0 è ORA disponbile! Scaricalo da",
|
"versionAlert": "termscp 1.0.0 è ORA disponbile! Scaricalo da",
|
||||||
"here": "qui",
|
"here": "qui",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "开始 →",
|
"getStarted": "开始 →",
|
||||||
"versionAlert": "termscp 0.19.0 现已发布! 从下载",
|
"versionAlert": "termscp 1.0.0 现已发布! 从下载",
|
||||||
"here": "这里",
|
"here": "这里",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
|
|||||||
+37
-21
@@ -94,7 +94,9 @@ impl ActivityManager {
|
|||||||
// local dir is remote_args.local_dir if set, otherwise current dir
|
// local dir is remote_args.local_dir if set, otherwise current dir
|
||||||
let local_dir = remote_args
|
let local_dir = remote_args
|
||||||
.local_dir
|
.local_dir
|
||||||
.unwrap_or_else(|| env::current_dir().unwrap());
|
.map(Ok)
|
||||||
|
.unwrap_or_else(env::current_dir)
|
||||||
|
.map_err(|err| format!("Could not resolve current directory: {err}"))?;
|
||||||
debug!("host bridge is None, setting local dir to {:?}", local_dir,);
|
debug!("host bridge is None, setting local dir to {:?}", local_dir,);
|
||||||
|
|
||||||
self.set_host_params(
|
self.set_host_params(
|
||||||
@@ -143,27 +145,26 @@ impl ActivityManager {
|
|||||||
|
|
||||||
match host {
|
match host {
|
||||||
HostParams::HostBridge(HostBridgeParams::Localhost(path)) => {
|
HostParams::HostBridge(HostBridgeParams::Localhost(path)) => {
|
||||||
self.context
|
self.context_mut()?
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.set_host_bridge_params(HostBridgeParams::Localhost(path));
|
.set_host_bridge_params(HostBridgeParams::Localhost(path));
|
||||||
}
|
}
|
||||||
HostParams::HostBridge(HostBridgeParams::Remote(_, _)) => {
|
HostParams::HostBridge(HostBridgeParams::Remote(_, _)) => {
|
||||||
let (protocol, params) = remote_params.unwrap();
|
let (protocol, params) = remote_params.ok_or_else(|| {
|
||||||
self.context
|
String::from("Missing remote parameters for host bridge configuration")
|
||||||
.as_mut()
|
})?;
|
||||||
.unwrap()
|
self.context_mut()?
|
||||||
.set_host_bridge_params(HostBridgeParams::Remote(protocol, params));
|
.set_host_bridge_params(HostBridgeParams::Remote(protocol, params));
|
||||||
}
|
}
|
||||||
HostParams::Remote(_) => {
|
HostParams::Remote(_) => {
|
||||||
let (protocol, params) = remote_params.unwrap();
|
let (protocol, params) = remote_params
|
||||||
|
.ok_or_else(|| String::from("Missing remote parameters for remote host"))?;
|
||||||
let params = FileTransferParams {
|
let params = FileTransferParams {
|
||||||
local_path: remote_local_path,
|
local_path: remote_local_path,
|
||||||
remote_path: remote_remote_path,
|
remote_path: remote_remote_path,
|
||||||
protocol,
|
protocol,
|
||||||
params,
|
params,
|
||||||
};
|
};
|
||||||
self.context.as_mut().unwrap().set_remote_params(params);
|
self.context_mut()?.set_remote_params(params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -185,16 +186,19 @@ impl ActivityManager {
|
|||||||
) && params.generic_params().is_some()
|
) && params.generic_params().is_some()
|
||||||
{
|
{
|
||||||
// * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password
|
// * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password
|
||||||
let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config());
|
let storage = SshKeyStorage::from(self.context_ref()?.config());
|
||||||
let generic_params = params.generic_params().unwrap();
|
let generic_params = params.generic_params().ok_or_else(|| {
|
||||||
|
String::from("Missing generic parameters for SSH password resolution")
|
||||||
|
})?;
|
||||||
|
let username = generic_params
|
||||||
|
.username
|
||||||
|
.clone()
|
||||||
|
.map(Ok)
|
||||||
|
.unwrap_or_else(whoami::username)
|
||||||
|
.map_err(|err| format!("Could not get current username: {err}"))?;
|
||||||
|
|
||||||
if storage
|
if storage
|
||||||
.resolve(
|
.resolve(&generic_params.address, &username)
|
||||||
&generic_params.address,
|
|
||||||
&generic_params
|
|
||||||
.username
|
|
||||||
.clone()
|
|
||||||
.unwrap_or(whoami::username()),
|
|
||||||
)
|
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
debug!(
|
debug!(
|
||||||
@@ -218,7 +222,7 @@ impl ActivityManager {
|
|||||||
|
|
||||||
/// Prompt user for password to set into params.
|
/// Prompt user for password to set into params.
|
||||||
fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> {
|
fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> {
|
||||||
let ctx = self.context.as_mut().unwrap();
|
let ctx = self.context_mut()?;
|
||||||
let prompt = format!("Password for {}: ", params.host_name());
|
let prompt = format!("Password for {}: ", params.host_name());
|
||||||
|
|
||||||
match tty::read_secret_from_tty(ctx.terminal(), prompt) {
|
match tty::read_secret_from_tty(ctx.terminal(), prompt) {
|
||||||
@@ -243,7 +247,7 @@ impl ActivityManager {
|
|||||||
bookmark_name: &str,
|
bookmark_name: &str,
|
||||||
password: Option<&str>,
|
password: Option<&str>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() {
|
if let Some(bookmarks_client) = self.context_mut()?.bookmarks_client_mut() {
|
||||||
let params = match bookmarks_client.get_bookmark(bookmark_name) {
|
let params = match bookmarks_client.get_bookmark(bookmark_name) {
|
||||||
None => {
|
None => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@@ -268,6 +272,18 @@ impl ActivityManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn context_mut(&mut self) -> Result<&mut Context, String> {
|
||||||
|
self.context
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| String::from("Activity manager context is not initialized"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context_ref(&self) -> Result<&Context, String> {
|
||||||
|
self.context
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| String::from("Activity manager context is not initialized"))
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Loop for activity manager. You need to provide the activity to start with
|
/// Loop for activity manager. You need to provide the activity to start with
|
||||||
/// Returns the exitcode
|
/// Returns the exitcode
|
||||||
|
|||||||
+19
-6
@@ -1,3 +1,8 @@
|
|||||||
|
//! ## Remote CLI Arguments
|
||||||
|
//!
|
||||||
|
//! Parses positional and bookmark-based CLI arguments into the normalized remote
|
||||||
|
//! connection parameters used by the application.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use super::Args;
|
use super::Args;
|
||||||
@@ -13,8 +18,11 @@ enum AddrType {
|
|||||||
/// Args for remote connection
|
/// Args for remote connection
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RemoteArgs {
|
pub struct RemoteArgs {
|
||||||
|
/// Optional host bridge selected for the session.
|
||||||
pub host_bridge: Remote,
|
pub host_bridge: Remote,
|
||||||
|
/// Target remote selected for the session.
|
||||||
pub remote: Remote,
|
pub remote: Remote,
|
||||||
|
/// Optional local working directory override.
|
||||||
pub local_dir: Option<PathBuf>,
|
pub local_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,9 +49,7 @@ impl TryFrom<&Args> for RemoteArgs {
|
|||||||
(_, _) => Err("Too many arguments".to_string()),
|
(_, _) => Err("Too many arguments".to_string()),
|
||||||
}?;
|
}?;
|
||||||
// parse bookmark first
|
// parse bookmark first
|
||||||
let last_item_index = (args.bookmark.len() + args.positional.len())
|
let last_item_index = (args.bookmark.len() + args.positional.len()).saturating_sub(1);
|
||||||
.checked_sub(1)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let mut hosts = vec![];
|
let mut hosts = vec![];
|
||||||
|
|
||||||
@@ -75,10 +81,16 @@ impl TryFrom<&Args> for RemoteArgs {
|
|||||||
|
|
||||||
// set args based on hosts len
|
// set args based on hosts len
|
||||||
if hosts.len() == 1 {
|
if hosts.len() == 1 {
|
||||||
remote_args.remote = hosts.pop().unwrap();
|
remote_args.remote = hosts
|
||||||
|
.pop()
|
||||||
|
.ok_or_else(|| String::from("Missing remote host configuration"))?;
|
||||||
} else if hosts.len() == 2 {
|
} else if hosts.len() == 2 {
|
||||||
remote_args.host_bridge = hosts.pop().unwrap();
|
remote_args.host_bridge = hosts
|
||||||
remote_args.remote = hosts.pop().unwrap();
|
.pop()
|
||||||
|
.ok_or_else(|| String::from("Missing host-bridge configuration"))?;
|
||||||
|
remote_args.remote = hosts
|
||||||
|
.pop()
|
||||||
|
.ok_or_else(|| String::from("Missing remote host configuration"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(remote_args)
|
Ok(remote_args)
|
||||||
@@ -105,6 +117,7 @@ pub enum Remote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Remote {
|
impl Remote {
|
||||||
|
/// Returns whether this CLI slot was left unspecified.
|
||||||
pub fn is_none(&self) -> bool {
|
pub fn is_none(&self) -> bool {
|
||||||
matches!(self, Self::None)
|
matches!(self, Self::None)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
//! ## Bookmark S3 Parameters
|
||||||
|
//!
|
||||||
|
//! Stores the bookmark-specific representation of AWS S3 connection settings.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::filetransfer::params::AwsS3Params;
|
use crate::filetransfer::params::AwsS3Params;
|
||||||
@@ -5,13 +9,20 @@ use crate::filetransfer::params::AwsS3Params;
|
|||||||
/// Connection parameters for Aws s3 protocol
|
/// Connection parameters for Aws s3 protocol
|
||||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||||
pub struct S3Params {
|
pub struct S3Params {
|
||||||
|
/// Bucket name to open.
|
||||||
pub bucket: String,
|
pub bucket: String,
|
||||||
|
/// AWS region used for the bucket.
|
||||||
pub region: Option<String>,
|
pub region: Option<String>,
|
||||||
|
/// Custom endpoint URL for S3-compatible services.
|
||||||
pub endpoint: Option<String>,
|
pub endpoint: Option<String>,
|
||||||
|
/// Shared credentials profile name.
|
||||||
pub profile: Option<String>,
|
pub profile: Option<String>,
|
||||||
|
/// Static access key identifier.
|
||||||
pub access_key: Option<String>,
|
pub access_key: Option<String>,
|
||||||
|
/// Static secret access key.
|
||||||
pub secret_access_key: Option<String>,
|
pub secret_access_key: Option<String>,
|
||||||
/// NOTE: there are no session token and security token since they are always temporary
|
/// NOTE: there are no session token and security token since they are always temporary
|
||||||
|
/// Whether to force path-style bucket addressing.
|
||||||
pub new_path_style: Option<bool>,
|
pub new_path_style: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
//! ## Bookmark Kube Parameters
|
||||||
|
//!
|
||||||
|
//! Stores bookmark-specific Kubernetes connection settings.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::filetransfer::params::KubeProtocolParams;
|
use crate::filetransfer::params::KubeProtocolParams;
|
||||||
@@ -5,10 +9,15 @@ use crate::filetransfer::params::KubeProtocolParams;
|
|||||||
/// Extra Connection parameters for Kube protocol
|
/// Extra Connection parameters for Kube protocol
|
||||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||||
pub struct KubeParams {
|
pub struct KubeParams {
|
||||||
|
/// Optional default namespace.
|
||||||
pub namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
|
/// Optional cluster API URL.
|
||||||
pub cluster_url: Option<String>,
|
pub cluster_url: Option<String>,
|
||||||
|
/// Optional Kubernetes username override.
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
|
/// Optional client certificate path.
|
||||||
pub client_cert: Option<String>,
|
pub client_cert: Option<String>,
|
||||||
|
/// Optional client key path.
|
||||||
pub client_key: Option<String>,
|
pub client_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
//! ## Bookmark SMB Parameters
|
||||||
|
//!
|
||||||
|
//! Stores bookmark-specific SMB share configuration.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::filetransfer::params::SmbParams as TransferSmbParams;
|
use crate::filetransfer::params::SmbParams as TransferSmbParams;
|
||||||
@@ -5,7 +9,9 @@ use crate::filetransfer::params::SmbParams as TransferSmbParams;
|
|||||||
/// Extra Connection parameters for SMB protocol
|
/// Extra Connection parameters for SMB protocol
|
||||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||||
pub struct SmbParams {
|
pub struct SmbParams {
|
||||||
|
/// SMB share name.
|
||||||
pub share: String,
|
pub share: String,
|
||||||
|
/// Optional SMB workgroup used on POSIX platforms.
|
||||||
pub workgroup: Option<String>,
|
pub workgroup: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -442,6 +442,34 @@ mod tests {
|
|||||||
assert!(deserialize::<UserHosts>(Box::new(toml_file)).is_err());
|
assert!(deserialize::<UserHosts>(Box::new(toml_file)).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_deserialize_webdav_bookmark_protocol_alias() {
|
||||||
|
let toml_file: tempfile::NamedTempFile = create_http_alias_toml_bookmarks();
|
||||||
|
toml_file.as_file().sync_all().unwrap();
|
||||||
|
toml_file.as_file().rewind().unwrap();
|
||||||
|
|
||||||
|
let hosts: UserHosts = deserialize(Box::new(toml_file)).unwrap();
|
||||||
|
let host = hosts.bookmarks.get("webdav").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(host.protocol, FileTransferProtocol::WebDAV);
|
||||||
|
assert_eq!(host.address.as_deref(), Some("https://myserver:4445"));
|
||||||
|
assert_eq!(host.username.as_deref(), Some("omar"));
|
||||||
|
assert_eq!(host.password.as_deref(), Some("mypassword"));
|
||||||
|
assert_eq!(
|
||||||
|
host.remote_path.as_deref(),
|
||||||
|
Some(std::path::Path::new("/myshare/dir/subdir"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_fail_deserialize_bookmark_with_invalid_protocol() {
|
||||||
|
let toml_file: tempfile::NamedTempFile = create_invalid_protocol_toml_bookmarks();
|
||||||
|
toml_file.as_file().sync_all().unwrap();
|
||||||
|
toml_file.as_file().rewind().unwrap();
|
||||||
|
|
||||||
|
assert!(deserialize::<UserHosts>(Box::new(toml_file)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_serializer_bookmarks_serializer_serialize() {
|
fn test_config_serializer_bookmarks_serializer_serialize() {
|
||||||
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(2);
|
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(2);
|
||||||
@@ -583,10 +611,12 @@ mod tests {
|
|||||||
toml_file.as_file().sync_all().unwrap();
|
toml_file.as_file().sync_all().unwrap();
|
||||||
toml_file.as_file().rewind().unwrap();
|
toml_file.as_file().rewind().unwrap();
|
||||||
assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok());
|
assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok());
|
||||||
|
// Malformed theme files must still load successfully; unknown or invalid
|
||||||
|
// fields fall back to defaults so user themes remain backwards compatible.
|
||||||
let toml_file = create_bad_toml_theme();
|
let toml_file = create_bad_toml_theme();
|
||||||
toml_file.as_file().sync_all().unwrap();
|
toml_file.as_file().sync_all().unwrap();
|
||||||
toml_file.as_file().rewind().unwrap();
|
toml_file.as_file().rewind().unwrap();
|
||||||
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err());
|
assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -693,6 +723,39 @@ mod tests {
|
|||||||
tmpfile
|
tmpfile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_http_alias_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||||
|
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
let file_content: &str = r#"
|
||||||
|
[bookmarks]
|
||||||
|
|
||||||
|
[bookmarks.webdav]
|
||||||
|
protocol = "HTTPS"
|
||||||
|
address = "https://myserver:4445"
|
||||||
|
username = "omar"
|
||||||
|
password = "mypassword"
|
||||||
|
directory = "/myshare/dir/subdir"
|
||||||
|
|
||||||
|
[recents]
|
||||||
|
"#;
|
||||||
|
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||||
|
tmpfile
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_invalid_protocol_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||||
|
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
let file_content: &str = r#"
|
||||||
|
[bookmarks]
|
||||||
|
|
||||||
|
[bookmarks.broken]
|
||||||
|
protocol = "GOPHER"
|
||||||
|
address = "gopher://myserver"
|
||||||
|
|
||||||
|
[recents]
|
||||||
|
"#;
|
||||||
|
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||||
|
tmpfile
|
||||||
|
}
|
||||||
|
|
||||||
fn create_good_toml_theme() -> tempfile::NamedTempFile {
|
fn create_good_toml_theme() -> tempfile::NamedTempFile {
|
||||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||||
let file_content: &str = r##"auth_address = "Yellow"
|
let file_content: &str = r##"auth_address = "Yellow"
|
||||||
@@ -714,8 +777,7 @@ mod tests {
|
|||||||
transfer_local_explorer_highlighted = "Yellow"
|
transfer_local_explorer_highlighted = "Yellow"
|
||||||
transfer_log_background = "255, 255, 255"
|
transfer_log_background = "255, 255, 255"
|
||||||
transfer_log_window = "LightGreen"
|
transfer_log_window = "LightGreen"
|
||||||
transfer_progress_bar_full = "forestgreen"
|
transfer_progress_bar = "forestgreen"
|
||||||
transfer_progress_bar_partial = "Green"
|
|
||||||
transfer_remote_explorer_background = "#f0f0f0"
|
transfer_remote_explorer_background = "#f0f0f0"
|
||||||
transfer_remote_explorer_foreground = "rgb(40, 40, 40)"
|
transfer_remote_explorer_foreground = "rgb(40, 40, 40)"
|
||||||
transfer_remote_explorer_highlighted = "LightBlue"
|
transfer_remote_explorer_highlighted = "LightBlue"
|
||||||
|
|||||||
+208
-126
@@ -12,145 +12,62 @@ use crate::utils::fmt::fmt_color;
|
|||||||
use crate::utils::parser::parse_color;
|
use crate::utils::parser::parse_color;
|
||||||
|
|
||||||
/// Theme contains all the colors lookup table for termscp
|
/// Theme contains all the colors lookup table for termscp
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
// -- auth
|
// -- auth
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub auth_address: Color,
|
pub auth_address: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub auth_bookmarks: Color,
|
pub auth_bookmarks: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub auth_password: Color,
|
pub auth_password: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub auth_port: Color,
|
pub auth_port: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub auth_protocol: Color,
|
pub auth_protocol: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub auth_recents: Color,
|
pub auth_recents: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub auth_username: Color,
|
pub auth_username: Color,
|
||||||
// -- misc
|
// -- misc
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub misc_error_dialog: Color,
|
pub misc_error_dialog: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub misc_info_dialog: Color,
|
pub misc_info_dialog: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub misc_input_dialog: Color,
|
pub misc_input_dialog: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub misc_keys: Color,
|
pub misc_keys: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub misc_quit_dialog: Color,
|
pub misc_quit_dialog: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub misc_save_dialog: Color,
|
pub misc_save_dialog: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub misc_warn_dialog: Color,
|
pub misc_warn_dialog: Color,
|
||||||
// -- transfer
|
// -- transfer
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_local_explorer_background: Color,
|
pub transfer_local_explorer_background: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_local_explorer_foreground: Color,
|
pub transfer_local_explorer_foreground: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_local_explorer_highlighted: Color,
|
pub transfer_local_explorer_highlighted: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_log_background: Color,
|
pub transfer_log_background: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_log_window: Color,
|
pub transfer_log_window: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
pub transfer_progress_bar: Color,
|
||||||
serialize_with = "serialize_color"
|
#[serde(serialize_with = "serialize_color")]
|
||||||
)]
|
|
||||||
pub transfer_progress_bar_full: Color,
|
|
||||||
#[serde(
|
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_progress_bar_partial: Color,
|
|
||||||
#[serde(
|
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_remote_explorer_background: Color,
|
pub transfer_remote_explorer_background: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_remote_explorer_foreground: Color,
|
pub transfer_remote_explorer_foreground: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_remote_explorer_highlighted: Color,
|
pub transfer_remote_explorer_highlighted: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_status_hidden: Color,
|
pub transfer_status_hidden: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_status_sorting: Color,
|
pub transfer_status_sorting: Color,
|
||||||
#[serde(
|
#[serde(serialize_with = "serialize_color")]
|
||||||
deserialize_with = "deserialize_color",
|
|
||||||
serialize_with = "serialize_color"
|
|
||||||
)]
|
|
||||||
pub transfer_status_sync_browsing: Color,
|
pub transfer_status_sync_browsing: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,8 +93,7 @@ impl Default for Theme {
|
|||||||
transfer_local_explorer_highlighted: Color::Yellow,
|
transfer_local_explorer_highlighted: Color::Yellow,
|
||||||
transfer_log_background: Color::Reset,
|
transfer_log_background: Color::Reset,
|
||||||
transfer_log_window: Color::LightGreen,
|
transfer_log_window: Color::LightGreen,
|
||||||
transfer_progress_bar_partial: Color::Green,
|
transfer_progress_bar: Color::Green,
|
||||||
transfer_progress_bar_full: Color::Green,
|
|
||||||
transfer_remote_explorer_background: Color::Reset,
|
transfer_remote_explorer_background: Color::Reset,
|
||||||
transfer_remote_explorer_foreground: Color::Reset,
|
transfer_remote_explorer_foreground: Color::Reset,
|
||||||
transfer_remote_explorer_highlighted: Color::LightBlue,
|
transfer_remote_explorer_highlighted: Color::LightBlue,
|
||||||
@@ -189,16 +105,124 @@ impl Default for Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- deserializer
|
// -- deserializer
|
||||||
|
//
|
||||||
|
// Custom deserialization: every field is optional and falls back to `Theme::default()`
|
||||||
|
// when missing or when the supplied color string is invalid. This keeps user themes
|
||||||
|
// backwards compatible even when fields are added, renamed, or contain typos.
|
||||||
|
|
||||||
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
|
#[derive(Deserialize, Default)]
|
||||||
where
|
#[serde(default)]
|
||||||
D: Deserializer<'de>,
|
struct ThemeFile {
|
||||||
{
|
auth_address: Option<String>,
|
||||||
let s: String = Deserialize::deserialize(deserializer)?;
|
auth_bookmarks: Option<String>,
|
||||||
// Parse color
|
auth_password: Option<String>,
|
||||||
match parse_color(&s) {
|
auth_port: Option<String>,
|
||||||
None => Err(DeError::custom("Invalid color")),
|
auth_protocol: Option<String>,
|
||||||
Some(color) => Ok(color),
|
auth_recents: Option<String>,
|
||||||
|
auth_username: Option<String>,
|
||||||
|
misc_error_dialog: Option<String>,
|
||||||
|
misc_info_dialog: Option<String>,
|
||||||
|
misc_input_dialog: Option<String>,
|
||||||
|
misc_keys: Option<String>,
|
||||||
|
misc_quit_dialog: Option<String>,
|
||||||
|
misc_save_dialog: Option<String>,
|
||||||
|
misc_warn_dialog: Option<String>,
|
||||||
|
transfer_local_explorer_background: Option<String>,
|
||||||
|
transfer_local_explorer_foreground: Option<String>,
|
||||||
|
transfer_local_explorer_highlighted: Option<String>,
|
||||||
|
transfer_log_background: Option<String>,
|
||||||
|
transfer_log_window: Option<String>,
|
||||||
|
transfer_progress_bar: Option<String>,
|
||||||
|
// Legacy aliases for the now-consolidated `transfer_progress_bar` field.
|
||||||
|
transfer_progress_bar_full: Option<String>,
|
||||||
|
transfer_progress_bar_partial: Option<String>,
|
||||||
|
transfer_remote_explorer_background: Option<String>,
|
||||||
|
transfer_remote_explorer_foreground: Option<String>,
|
||||||
|
transfer_remote_explorer_highlighted: Option<String>,
|
||||||
|
transfer_status_hidden: Option<String>,
|
||||||
|
transfer_status_sorting: Option<String>,
|
||||||
|
transfer_status_sync_browsing: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeFile {
|
||||||
|
fn into_theme(self) -> Theme {
|
||||||
|
let defaults = Theme::default();
|
||||||
|
fn pick(value: Option<String>, fallback: Color) -> Color {
|
||||||
|
value.as_deref().and_then(parse_color).unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
Theme {
|
||||||
|
auth_address: pick(self.auth_address, defaults.auth_address),
|
||||||
|
auth_bookmarks: pick(self.auth_bookmarks, defaults.auth_bookmarks),
|
||||||
|
auth_password: pick(self.auth_password, defaults.auth_password),
|
||||||
|
auth_port: pick(self.auth_port, defaults.auth_port),
|
||||||
|
auth_protocol: pick(self.auth_protocol, defaults.auth_protocol),
|
||||||
|
auth_recents: pick(self.auth_recents, defaults.auth_recents),
|
||||||
|
auth_username: pick(self.auth_username, defaults.auth_username),
|
||||||
|
misc_error_dialog: pick(self.misc_error_dialog, defaults.misc_error_dialog),
|
||||||
|
misc_info_dialog: pick(self.misc_info_dialog, defaults.misc_info_dialog),
|
||||||
|
misc_input_dialog: pick(self.misc_input_dialog, defaults.misc_input_dialog),
|
||||||
|
misc_keys: pick(self.misc_keys, defaults.misc_keys),
|
||||||
|
misc_quit_dialog: pick(self.misc_quit_dialog, defaults.misc_quit_dialog),
|
||||||
|
misc_save_dialog: pick(self.misc_save_dialog, defaults.misc_save_dialog),
|
||||||
|
misc_warn_dialog: pick(self.misc_warn_dialog, defaults.misc_warn_dialog),
|
||||||
|
transfer_local_explorer_background: pick(
|
||||||
|
self.transfer_local_explorer_background,
|
||||||
|
defaults.transfer_local_explorer_background,
|
||||||
|
),
|
||||||
|
transfer_local_explorer_foreground: pick(
|
||||||
|
self.transfer_local_explorer_foreground,
|
||||||
|
defaults.transfer_local_explorer_foreground,
|
||||||
|
),
|
||||||
|
transfer_local_explorer_highlighted: pick(
|
||||||
|
self.transfer_local_explorer_highlighted,
|
||||||
|
defaults.transfer_local_explorer_highlighted,
|
||||||
|
),
|
||||||
|
transfer_log_background: pick(
|
||||||
|
self.transfer_log_background,
|
||||||
|
defaults.transfer_log_background,
|
||||||
|
),
|
||||||
|
transfer_log_window: pick(self.transfer_log_window, defaults.transfer_log_window),
|
||||||
|
transfer_progress_bar: pick(
|
||||||
|
self.transfer_progress_bar
|
||||||
|
.or(self.transfer_progress_bar_full)
|
||||||
|
.or(self.transfer_progress_bar_partial),
|
||||||
|
defaults.transfer_progress_bar,
|
||||||
|
),
|
||||||
|
transfer_remote_explorer_background: pick(
|
||||||
|
self.transfer_remote_explorer_background,
|
||||||
|
defaults.transfer_remote_explorer_background,
|
||||||
|
),
|
||||||
|
transfer_remote_explorer_foreground: pick(
|
||||||
|
self.transfer_remote_explorer_foreground,
|
||||||
|
defaults.transfer_remote_explorer_foreground,
|
||||||
|
),
|
||||||
|
transfer_remote_explorer_highlighted: pick(
|
||||||
|
self.transfer_remote_explorer_highlighted,
|
||||||
|
defaults.transfer_remote_explorer_highlighted,
|
||||||
|
),
|
||||||
|
transfer_status_hidden: pick(
|
||||||
|
self.transfer_status_hidden,
|
||||||
|
defaults.transfer_status_hidden,
|
||||||
|
),
|
||||||
|
transfer_status_sorting: pick(
|
||||||
|
self.transfer_status_sorting,
|
||||||
|
defaults.transfer_status_sorting,
|
||||||
|
),
|
||||||
|
transfer_status_sync_browsing: pick(
|
||||||
|
self.transfer_status_sync_browsing,
|
||||||
|
defaults.transfer_status_sync_browsing,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Theme {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let file = ThemeFile::deserialize(deserializer)?;
|
||||||
|
Ok(file.into_theme())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,14 +235,29 @@ where
|
|||||||
serializer.serialize_str(s.as_str())
|
serializer.serialize_str(s.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kept for backwards compatibility with any external callers; no longer used directly
|
||||||
|
// by serde derive because `Theme` now uses a custom `Deserialize` impl.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
match parse_color(&s) {
|
||||||
|
None => Err(DeError::custom("Invalid color")),
|
||||||
|
Some(color) => Ok(color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod tests {
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_themes_default() {
|
fn should_get_default_theme() {
|
||||||
let theme: Theme = Theme::default();
|
let theme: Theme = Theme::default();
|
||||||
assert_eq!(theme.auth_address, Color::Yellow);
|
assert_eq!(theme.auth_address, Color::Yellow);
|
||||||
assert_eq!(theme.auth_bookmarks, Color::LightGreen);
|
assert_eq!(theme.auth_bookmarks, Color::LightGreen);
|
||||||
@@ -239,8 +278,7 @@ mod test {
|
|||||||
assert_eq!(theme.transfer_local_explorer_highlighted, Color::Yellow);
|
assert_eq!(theme.transfer_local_explorer_highlighted, Color::Yellow);
|
||||||
assert_eq!(theme.transfer_log_background, Color::Reset);
|
assert_eq!(theme.transfer_log_background, Color::Reset);
|
||||||
assert_eq!(theme.transfer_log_window, Color::LightGreen);
|
assert_eq!(theme.transfer_log_window, Color::LightGreen);
|
||||||
assert_eq!(theme.transfer_progress_bar_full, Color::Green);
|
assert_eq!(theme.transfer_progress_bar, Color::Green);
|
||||||
assert_eq!(theme.transfer_progress_bar_partial, Color::Green);
|
|
||||||
assert_eq!(theme.transfer_remote_explorer_background, Color::Reset);
|
assert_eq!(theme.transfer_remote_explorer_background, Color::Reset);
|
||||||
assert_eq!(theme.transfer_remote_explorer_foreground, Color::Reset);
|
assert_eq!(theme.transfer_remote_explorer_foreground, Color::Reset);
|
||||||
assert_eq!(theme.transfer_remote_explorer_highlighted, Color::LightBlue);
|
assert_eq!(theme.transfer_remote_explorer_highlighted, Color::LightBlue);
|
||||||
@@ -248,4 +286,48 @@ mod test {
|
|||||||
assert_eq!(theme.transfer_status_sorting, Color::LightYellow);
|
assert_eq!(theme.transfer_status_sorting, Color::LightYellow);
|
||||||
assert_eq!(theme.transfer_status_sync_browsing, Color::LightGreen);
|
assert_eq!(theme.transfer_status_sync_browsing, Color::LightGreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_accept_legacy_progress_bar_fields() {
|
||||||
|
let toml = r#"
|
||||||
|
auth_protocol = "Yellow"
|
||||||
|
transfer_progress_bar_full = "Green"
|
||||||
|
"#;
|
||||||
|
let theme: Theme = toml::from_str(toml).expect("theme should load");
|
||||||
|
assert_eq!(theme.auth_protocol, Color::Yellow);
|
||||||
|
assert_eq!(theme.transfer_progress_bar, Color::Green);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_ignore_duplicated_legacy_progress_bar_fields() {
|
||||||
|
let toml = r#"
|
||||||
|
auth_protocol = "Yellow"
|
||||||
|
transfer_progress_bar_full = "Green"
|
||||||
|
transfer_progress_bar_partial = "Red"
|
||||||
|
"#;
|
||||||
|
let theme: Theme = toml::from_str(toml).expect("theme should load");
|
||||||
|
assert_eq!(theme.auth_protocol, Color::Yellow);
|
||||||
|
// `_full` wins because `transfer_progress_bar` and `_full` are checked first.
|
||||||
|
assert_eq!(theme.transfer_progress_bar, Color::Green);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_fall_back_to_defaults_on_invalid_values() {
|
||||||
|
// Invalid color value should make *that* field fall back to default, without
|
||||||
|
// breaking the rest of the theme.
|
||||||
|
let toml = r##"
|
||||||
|
auth_protocol = "not-a-color"
|
||||||
|
auth_username = "#ca9ee6"
|
||||||
|
"##;
|
||||||
|
let theme: Theme = toml::from_str(toml).expect("theme should load");
|
||||||
|
assert_eq!(theme.auth_protocol, Theme::default().auth_protocol);
|
||||||
|
assert_eq!(theme.auth_username, Color::Rgb(202, 158, 230));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_fall_back_to_defaults_on_missing_fields() {
|
||||||
|
// Empty file should still produce a theme equal to default.
|
||||||
|
let theme: Theme = toml::from_str("").expect("theme should load");
|
||||||
|
assert_eq!(theme, Theme::default());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,7 +245,8 @@ impl FileExplorer {
|
|||||||
|
|
||||||
/// Sort explorer files by their name. All names are converted to lowercase
|
/// Sort explorer files by their name. All names are converted to lowercase
|
||||||
fn sort_files_by_name(&mut self) {
|
fn sort_files_by_name(&mut self) {
|
||||||
self.files.sort_by_key(|x: &File| x.name().to_lowercase());
|
self.files
|
||||||
|
.sort_by_cached_key(|x: &File| x.name().to_lowercase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sort files by mtime; the newest comes first
|
/// Sort files by mtime; the newest comes first
|
||||||
@@ -24,7 +24,7 @@ impl FileExplorerBuilder {
|
|||||||
|
|
||||||
/// Take FileExplorer out of builder
|
/// Take FileExplorer out of builder
|
||||||
pub fn build(&mut self) -> FileExplorer {
|
pub fn build(&mut self) -> FileExplorer {
|
||||||
self.explorer.take().unwrap()
|
self.explorer.take().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable HIDDEN_FILES option
|
/// Enable HIDDEN_FILES option
|
||||||
|
|||||||
+41
-21
@@ -108,7 +108,7 @@ impl CallChainBlock {
|
|||||||
None => {
|
None => {
|
||||||
self.next_block = Some(Box::new(CallChainBlock::new(
|
self.next_block = Some(Box::new(CallChainBlock::new(
|
||||||
func, prefix, fmt_len, fmt_extra,
|
func, prefix, fmt_len, fmt_extra,
|
||||||
)))
|
)));
|
||||||
}
|
}
|
||||||
Some(block) => block.push(func, prefix, fmt_len, fmt_extra),
|
Some(block) => block.push(func, prefix, fmt_len, fmt_extra),
|
||||||
}
|
}
|
||||||
@@ -375,23 +375,20 @@ impl Formatter {
|
|||||||
// Add to cur str, prefix and the key value
|
// Add to cur str, prefix and the key value
|
||||||
//format!("{cur_str}{prefix}{size:10}", size = size.display().si())
|
//format!("{cur_str}{prefix}{size:10}", size = size.display().si())
|
||||||
} else if fsentry.metadata().symlink.is_some() {
|
} else if fsentry.metadata().symlink.is_some() {
|
||||||
let size = ByteSize(
|
match fsentry.metadata().symlink.as_ref() {
|
||||||
fsentry
|
Some(symlink) => {
|
||||||
.metadata()
|
let size = ByteSize(symlink.to_string_lossy().len() as u64);
|
||||||
.symlink
|
let mut fmt = size.display().si().to_string();
|
||||||
.as_ref()
|
// pad with up to len 10
|
||||||
.unwrap()
|
let pad = 10usize.saturating_sub(fmt.len());
|
||||||
.to_string_lossy()
|
for _ in 0..pad {
|
||||||
.len() as u64,
|
fmt.push(' ');
|
||||||
);
|
}
|
||||||
let mut fmt = size.display().si().to_string();
|
|
||||||
// pad with up to len 10
|
|
||||||
let pad = 10usize.saturating_sub(fmt.len());
|
|
||||||
for _ in 0..pad {
|
|
||||||
fmt.push(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("{cur_str}{prefix}{fmt}")
|
format!("{cur_str}{prefix}{fmt}")
|
||||||
|
}
|
||||||
|
None => format!("{cur_str}{prefix} "),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Add to cur str, prefix and the key value
|
// Add to cur str, prefix and the key value
|
||||||
format!("{cur_str}{prefix} ")
|
format!("{cur_str}{prefix} ")
|
||||||
@@ -476,12 +473,14 @@ impl Formatter {
|
|||||||
let mut last_index: usize = 0;
|
let mut last_index: usize = 0;
|
||||||
// Match fmt str against regex
|
// Match fmt str against regex
|
||||||
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
|
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
|
||||||
// Get match index (unwrap is safe, since always exists)
|
let Some(full_match) = regex_match.get(0) else {
|
||||||
let index: usize = fmt_str.find(®ex_match[0]).unwrap();
|
continue;
|
||||||
|
};
|
||||||
|
let index: usize = full_match.start();
|
||||||
// Get prefix
|
// Get prefix
|
||||||
let prefix: String = String::from(&fmt_str[last_index..index]);
|
let prefix: String = String::from(&fmt_str[last_index..index]);
|
||||||
// Increment last index (sum prefix lenght and the length of the key)
|
// Increment last index (sum prefix lenght and the length of the key)
|
||||||
last_index += prefix.len() + regex_match[0].len();
|
last_index += prefix.len() + full_match.as_str().len();
|
||||||
// Match attributes
|
// Match attributes
|
||||||
match FMT_ATTR_REGEX.captures(®ex_match[1]) {
|
match FMT_ATTR_REGEX.captures(®ex_match[1]) {
|
||||||
Some(regex_match) => {
|
Some(regex_match) => {
|
||||||
@@ -516,7 +515,7 @@ impl Formatter {
|
|||||||
match callchain.as_mut() {
|
match callchain.as_mut() {
|
||||||
None => {
|
None => {
|
||||||
callchain =
|
callchain =
|
||||||
Some(CallChainBlock::new(callback, prefix, fmt_len, fmt_extra))
|
Some(CallChainBlock::new(callback, prefix, fmt_len, fmt_extra));
|
||||||
}
|
}
|
||||||
Some(chain_block) => chain_block.push(callback, prefix, fmt_len, fmt_extra),
|
Some(chain_block) => chain_block.push(callback, prefix, fmt_len, fmt_extra),
|
||||||
}
|
}
|
||||||
@@ -1027,6 +1026,27 @@ mod tests {
|
|||||||
assert_eq!(formatter.fmt(&entry).as_str(), "喵喵喵喵喵喵喵…");
|
assert_eq!(formatter.fmt(&entry).as_str(), "喵喵喵喵喵喵喵…");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_ignore_unknown_formatter_keys() {
|
||||||
|
let entry = File {
|
||||||
|
path: PathBuf::from("/tmp/foo.txt"),
|
||||||
|
metadata: Metadata {
|
||||||
|
accessed: None,
|
||||||
|
created: None,
|
||||||
|
modified: None,
|
||||||
|
file_type: FileType::File,
|
||||||
|
size: 8192,
|
||||||
|
symlink: None,
|
||||||
|
uid: None,
|
||||||
|
gid: None,
|
||||||
|
mode: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let formatter: Formatter = Formatter::new("before {UNKNOWN:12} after {NAME:8}");
|
||||||
|
|
||||||
|
assert_eq!(formatter.fmt(&entry).as_str(), "before after foo.txt ");
|
||||||
|
}
|
||||||
|
|
||||||
/// Dummy formatter, just yelds an 'A' at the end of the current string
|
/// Dummy formatter, just yelds an 'A' at the end of the current string
|
||||||
fn dummy_fmt(
|
fn dummy_fmt(
|
||||||
_fmt: &Formatter,
|
_fmt: &Formatter,
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
//! ## Host Bridge Builder
|
||||||
|
//!
|
||||||
|
//! Builds host bridge implementations from persisted host bridge parameters and
|
||||||
|
//! the active configuration client.
|
||||||
|
|
||||||
use super::{HostBridgeParams, RemoteFsBuilder};
|
use super::{HostBridgeParams, RemoteFsBuilder};
|
||||||
use crate::host::{HostBridge, Localhost, RemoteBridged};
|
use crate::host::{HostBridge, Localhost, RemoteBridged};
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
|
|
||||||
|
/// Builds the host-side filesystem bridge used during file transfer sessions.
|
||||||
pub struct HostBridgeBuilder;
|
pub struct HostBridgeBuilder;
|
||||||
|
|
||||||
impl HostBridgeBuilder {
|
impl HostBridgeBuilder {
|
||||||
/// Build Host Bridge from parms
|
/// Builds a host bridge from serialized parameters.
|
||||||
///
|
///
|
||||||
/// if protocol and parameters are inconsistent, the function will return an error.
|
/// Returns an error when the selected host protocol and parameters are inconsistent.
|
||||||
pub fn build(
|
pub fn build(
|
||||||
params: HostBridgeParams,
|
params: HostBridgeParams,
|
||||||
config_client: &ConfigClient,
|
config_client: &ConfigClient,
|
||||||
|
|||||||
@@ -25,17 +25,17 @@ pub enum HostBridgeParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HostBridgeParams {
|
impl HostBridgeParams {
|
||||||
pub fn unwrap_protocol_params(&self) -> &ProtocolParams {
|
pub fn protocol_params(&self) -> Option<&ProtocolParams> {
|
||||||
match self {
|
match self {
|
||||||
HostBridgeParams::Localhost(_) => panic!("Localhost has no protocol params"),
|
HostBridgeParams::Localhost(_) => None,
|
||||||
HostBridgeParams::Remote(_, params) => params,
|
HostBridgeParams::Remote(_, params) => Some(params),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the host name for the bridge params
|
/// Returns the host name for the bridge params
|
||||||
pub fn username(&self) -> Option<String> {
|
pub fn username(&self) -> Option<String> {
|
||||||
match self {
|
match self {
|
||||||
HostBridgeParams::Localhost(_) => Some(whoami::username()),
|
HostBridgeParams::Localhost(_) => whoami::username().ok(),
|
||||||
HostBridgeParams::Remote(_, params) => {
|
HostBridgeParams::Remote(_, params) => {
|
||||||
params.generic_params().and_then(|p| p.username.clone())
|
params.generic_params().and_then(|p| p.username.clone())
|
||||||
}
|
}
|
||||||
@@ -278,6 +278,19 @@ mod test {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_protocol_params_on_localhost_returns_none() {
|
||||||
|
let params = HostBridgeParams::Localhost(PathBuf::from("/tmp"));
|
||||||
|
assert!(params.protocol_params().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_protocol_params_on_remote_returns_some() {
|
||||||
|
let params =
|
||||||
|
HostBridgeParams::Remote(FileTransferProtocol::Sftp, ProtocolParams::default());
|
||||||
|
assert!(params.protocol_params().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_filetransfer_params() {
|
fn test_filetransfer_params() {
|
||||||
let params: FileTransferParams =
|
let params: FileTransferParams =
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
|
//! ## AWS S3 Parameters
|
||||||
|
//!
|
||||||
|
//! Defines the runtime connection parameters used to build AWS S3 and
|
||||||
|
//! S3-compatible remote filesystem clients.
|
||||||
|
|
||||||
/// Connection parameters for AWS S3 protocol
|
/// Connection parameters for AWS S3 protocol
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AwsS3Params {
|
pub struct AwsS3Params {
|
||||||
|
/// Target bucket name.
|
||||||
pub bucket_name: String,
|
pub bucket_name: String,
|
||||||
|
/// Target region.
|
||||||
pub region: Option<String>,
|
pub region: Option<String>,
|
||||||
|
/// Optional custom endpoint URL.
|
||||||
pub endpoint: Option<String>,
|
pub endpoint: Option<String>,
|
||||||
|
/// Optional shared credentials profile.
|
||||||
pub profile: Option<String>,
|
pub profile: Option<String>,
|
||||||
|
/// Optional static access key.
|
||||||
pub access_key: Option<String>,
|
pub access_key: Option<String>,
|
||||||
|
/// Optional static secret access key.
|
||||||
pub secret_access_key: Option<String>,
|
pub secret_access_key: Option<String>,
|
||||||
|
/// Optional security token for temporary credentials.
|
||||||
pub security_token: Option<String>,
|
pub security_token: Option<String>,
|
||||||
|
/// Optional session token for temporary credentials.
|
||||||
pub session_token: Option<String>,
|
pub session_token: Option<String>,
|
||||||
|
/// Whether to force path-style bucket addressing.
|
||||||
pub new_path_style: bool,
|
pub new_path_style: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
|
//! ## Kubernetes Parameters
|
||||||
|
//!
|
||||||
|
//! Defines the runtime parameters used to construct Kubernetes-backed file
|
||||||
|
//! transfer clients.
|
||||||
|
|
||||||
use remotefs_kube::Config;
|
use remotefs_kube::Config;
|
||||||
|
|
||||||
/// Protocol params used by WebDAV
|
/// Protocol params used by Kubernetes connections.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct KubeProtocolParams {
|
pub struct KubeProtocolParams {
|
||||||
|
/// Optional namespace for the default pod context.
|
||||||
pub namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
|
/// Optional Kubernetes API URL.
|
||||||
pub cluster_url: Option<String>,
|
pub cluster_url: Option<String>,
|
||||||
|
/// Optional username override.
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
|
/// Optional client certificate path.
|
||||||
pub client_cert: Option<String>,
|
pub client_cert: Option<String>,
|
||||||
|
/// Optional client key path.
|
||||||
pub client_key: Option<String>,
|
pub client_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KubeProtocolParams {
|
impl KubeProtocolParams {
|
||||||
|
/// Kubernetes connections do not use the shared password secret flow.
|
||||||
pub fn set_default_secret(&mut self, _secret: String) {}
|
pub fn set_default_secret(&mut self, _secret: String) {}
|
||||||
|
|
||||||
|
/// Kubernetes params never require the generic password prompt.
|
||||||
pub fn password_missing(&self) -> bool {
|
pub fn password_missing(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts bookmark/runtime parameters into a `remotefs_kube` config.
|
||||||
pub fn config(self) -> Option<Config> {
|
pub fn config(self) -> Option<Config> {
|
||||||
if let Some(cluster_url) = self.cluster_url {
|
if let Some(cluster_url) = self.cluster_url {
|
||||||
let mut config = Config::new(cluster_url.parse().unwrap_or_default());
|
let mut config = Config::new(cluster_url.parse().unwrap_or_default());
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
|
//! ## SMB Parameters
|
||||||
|
//!
|
||||||
|
//! Defines the runtime connection parameters used to build SMB remote
|
||||||
|
//! filesystem clients.
|
||||||
|
|
||||||
/// Connection parameters for SMB protocol
|
/// Connection parameters for SMB protocol
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SmbParams {
|
pub struct SmbParams {
|
||||||
|
/// Hostname or address of the SMB server.
|
||||||
pub address: String,
|
pub address: String,
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
|
/// SMB service port used on POSIX platforms.
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
/// Share name to mount.
|
||||||
pub share: String,
|
pub share: String,
|
||||||
|
/// Optional username.
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
|
/// Optional password.
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
|
/// Optional workgroup used on POSIX platforms.
|
||||||
pub workgroup: Option<String>,
|
pub workgroup: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
|
//! ## WebDAV Parameters
|
||||||
|
//!
|
||||||
|
//! Defines the runtime connection parameters used to build WebDAV clients.
|
||||||
|
|
||||||
/// Protocol params used by WebDAV
|
/// Protocol params used by WebDAV
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct WebDAVProtocolParams {
|
pub struct WebDAVProtocolParams {
|
||||||
|
/// Base WebDAV endpoint URI.
|
||||||
pub uri: String,
|
pub uri: String,
|
||||||
|
/// Username used for authentication.
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
/// Password used for authentication.
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebDAVProtocolParams {
|
impl WebDAVProtocolParams {
|
||||||
|
/// Stores the shared secret as the active WebDAV password.
|
||||||
pub fn set_default_secret(&mut self, secret: String) {
|
pub fn set_default_secret(&mut self, secret: String) {
|
||||||
self.password = secret;
|
self.password = secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether the WebDAV password is currently missing.
|
||||||
pub fn password_missing(&self) -> bool {
|
pub fn password_missing(&self) -> bool {
|
||||||
self.password.is_empty()
|
self.password.is_empty()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ use remotefs_kube::KubeMultiPodFs as KubeFs;
|
|||||||
use remotefs_smb::SmbOptions;
|
use remotefs_smb::SmbOptions;
|
||||||
#[cfg(smb)]
|
#[cfg(smb)]
|
||||||
use remotefs_smb::{SmbCredentials, SmbFs};
|
use remotefs_smb::{SmbCredentials, SmbFs};
|
||||||
#[cfg(windows)]
|
use remotefs_ssh::{
|
||||||
use remotefs_ssh::LibSsh2Session as SshSession;
|
NoCheckServerKey, RusshSession as SshSession, ScpFs, SftpFs, SshAgentIdentity,
|
||||||
#[cfg(unix)]
|
SshConfigParseRule, SshOpts,
|
||||||
use remotefs_ssh::LibSshSession as SshSession;
|
};
|
||||||
use remotefs_ssh::{ScpFs, SftpFs, SshAgentIdentity, SshConfigParseRule, SshOpts};
|
|
||||||
use remotefs_webdav::WebDAVFs;
|
use remotefs_webdav::WebDAVFs;
|
||||||
|
|
||||||
#[cfg(not(smb))]
|
#[cfg(not(smb))]
|
||||||
@@ -44,23 +43,23 @@ impl RemoteFsBuilder {
|
|||||||
) -> Result<Box<dyn RemoteFs>, String> {
|
) -> Result<Box<dyn RemoteFs>, String> {
|
||||||
match (protocol, params) {
|
match (protocol, params) {
|
||||||
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => {
|
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => {
|
||||||
Ok(Box::new(Self::aws_s3_client(params)))
|
Ok(Box::new(Self::aws_s3_client(params)?))
|
||||||
}
|
}
|
||||||
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
|
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
|
||||||
Ok(Box::new(Self::ftp_client(params, secure)))
|
Ok(Box::new(Self::ftp_client(params, secure)))
|
||||||
}
|
}
|
||||||
(FileTransferProtocol::Kube, ProtocolParams::Kube(params)) => {
|
(FileTransferProtocol::Kube, ProtocolParams::Kube(params)) => {
|
||||||
Ok(Box::new(Self::kube_client(params)))
|
Ok(Box::new(Self::kube_client(params)?))
|
||||||
}
|
}
|
||||||
(FileTransferProtocol::Scp, ProtocolParams::Generic(params)) => {
|
(FileTransferProtocol::Scp, ProtocolParams::Generic(params)) => {
|
||||||
Ok(Box::new(Self::scp_client(params, config_client)))
|
Ok(Box::new(Self::scp_client(params, config_client)?))
|
||||||
}
|
}
|
||||||
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
|
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
|
||||||
Ok(Box::new(Self::sftp_client(params, config_client)))
|
Ok(Box::new(Self::sftp_client(params, config_client)?))
|
||||||
}
|
}
|
||||||
#[cfg(smb)]
|
#[cfg(smb)]
|
||||||
(FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => {
|
(FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => {
|
||||||
Ok(Box::new(Self::smb_client(params)))
|
Ok(Box::new(Self::smb_client(params)?))
|
||||||
}
|
}
|
||||||
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
|
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
|
||||||
Ok(Box::new(Self::webdav_client(params)))
|
Ok(Box::new(Self::webdav_client(params)))
|
||||||
@@ -75,13 +74,13 @@ impl RemoteFsBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build aws s3 client from parameters
|
/// Build aws s3 client from parameters
|
||||||
fn aws_s3_client(params: AwsS3Params) -> AwsS3Fs {
|
fn aws_s3_client(params: AwsS3Params) -> Result<AwsS3Fs, String> {
|
||||||
let rt = Arc::new(
|
let rt = Arc::new(
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
.worker_threads(1)
|
.worker_threads(1)
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.expect("Unable to create tokio runtime"),
|
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
|
||||||
);
|
);
|
||||||
let mut client =
|
let mut client =
|
||||||
AwsS3Fs::new(params.bucket_name, &rt).new_path_style(params.new_path_style);
|
AwsS3Fs::new(params.bucket_name, &rt).new_path_style(params.new_path_style);
|
||||||
@@ -106,7 +105,7 @@ impl RemoteFsBuilder {
|
|||||||
if let Some(session_token) = params.session_token {
|
if let Some(session_token) = params.session_token {
|
||||||
client = client.session_token(session_token);
|
client = client.session_token(session_token);
|
||||||
}
|
}
|
||||||
client
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build ftp client from parameters
|
/// Build ftp client from parameters
|
||||||
@@ -125,19 +124,13 @@ impl RemoteFsBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build kube client
|
/// Build kube client
|
||||||
fn kube_client(params: KubeProtocolParams) -> KubeFs {
|
fn kube_client(params: KubeProtocolParams) -> Result<KubeFs, String> {
|
||||||
let rt = Arc::new(
|
let rt = Self::tokio_runtime()?;
|
||||||
tokio::runtime::Builder::new_current_thread()
|
|
||||||
.worker_threads(1)
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.expect("Unable to create tokio runtime"),
|
|
||||||
);
|
|
||||||
let kube_fs = KubeFs::new(&rt);
|
let kube_fs = KubeFs::new(&rt);
|
||||||
if let Some(config) = params.config() {
|
if let Some(config) = params.config() {
|
||||||
kube_fs.config(config)
|
Ok(kube_fs.config(config))
|
||||||
} else {
|
} else {
|
||||||
kube_fs
|
Ok(kube_fs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,20 +138,24 @@ impl RemoteFsBuilder {
|
|||||||
fn scp_client(
|
fn scp_client(
|
||||||
params: GenericProtocolParams,
|
params: GenericProtocolParams,
|
||||||
config_client: &ConfigClient,
|
config_client: &ConfigClient,
|
||||||
) -> ScpFs<SshSession> {
|
) -> Result<ScpFs<SshSession<NoCheckServerKey>>, String> {
|
||||||
Self::build_ssh_opts(params, config_client).into()
|
let opts = Self::build_ssh_opts(params, config_client);
|
||||||
|
let rt = Self::tokio_runtime()?;
|
||||||
|
Ok(ScpFs::russh(opts, rt))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build sftp client
|
/// Build sftp client
|
||||||
fn sftp_client(
|
fn sftp_client(
|
||||||
params: GenericProtocolParams,
|
params: GenericProtocolParams,
|
||||||
config_client: &ConfigClient,
|
config_client: &ConfigClient,
|
||||||
) -> SftpFs<SshSession> {
|
) -> Result<SftpFs<SshSession<NoCheckServerKey>>, String> {
|
||||||
Self::build_ssh_opts(params, config_client).into()
|
let opts = Self::build_ssh_opts(params, config_client);
|
||||||
|
let rt = Self::tokio_runtime()?;
|
||||||
|
Ok(SftpFs::russh(opts, rt))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(smb_unix)]
|
#[cfg(smb_unix)]
|
||||||
fn smb_client(params: SmbParams) -> SmbFs {
|
fn smb_client(params: SmbParams) -> Result<SmbFs, String> {
|
||||||
let mut credentials = SmbCredentials::default()
|
let mut credentials = SmbCredentials::default()
|
||||||
.server(format!("smb://{}:{}", params.address, params.port))
|
.server(format!("smb://{}:{}", params.address, params.port))
|
||||||
.share(params.share);
|
.share(params.share);
|
||||||
@@ -173,22 +170,20 @@ impl RemoteFsBuilder {
|
|||||||
credentials = credentials.workgroup(workgroup);
|
credentials = credentials.workgroup(workgroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
match SmbFs::try_new(
|
SmbFs::try_new(
|
||||||
credentials,
|
credentials,
|
||||||
SmbOptions::default()
|
SmbOptions::default()
|
||||||
.one_share_per_server(true)
|
.one_share_per_server(true)
|
||||||
.case_sensitive(false),
|
.case_sensitive(false),
|
||||||
) {
|
)
|
||||||
Ok(fs) => fs,
|
.map_err(|e| {
|
||||||
Err(e) => {
|
error!("Invalid params for protocol SMB: {e}");
|
||||||
error!("Invalid params for protocol SMB: {e}");
|
format!("Invalid params for protocol SMB: {e}")
|
||||||
panic!("Invalid params for protocol SMB: {e}")
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(smb_windows)]
|
#[cfg(smb_windows)]
|
||||||
fn smb_client(params: SmbParams) -> SmbFs {
|
fn smb_client(params: SmbParams) -> Result<SmbFs, String> {
|
||||||
let mut credentials = SmbCredentials::new(params.address, params.share);
|
let mut credentials = SmbCredentials::new(params.address, params.share);
|
||||||
|
|
||||||
if let Some(username) = params.username {
|
if let Some(username) = params.username {
|
||||||
@@ -198,7 +193,7 @@ impl RemoteFsBuilder {
|
|||||||
credentials = credentials.password(password);
|
credentials = credentials.password(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
SmbFs::new(credentials)
|
Ok(SmbFs::new(credentials))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn webdav_client(params: WebDAVProtocolParams) -> WebDAVFs {
|
fn webdav_client(params: WebDAVProtocolParams) -> WebDAVFs {
|
||||||
@@ -236,12 +231,13 @@ impl RemoteFsBuilder {
|
|||||||
} else {
|
} else {
|
||||||
//* case 3: use system username; can't be None
|
//* case 3: use system username; can't be None
|
||||||
debug!("no username was provided, using current username");
|
debug!("no username was provided, using current username");
|
||||||
opts = opts.username(whoami::username());
|
if let Ok(username) = whoami::username() {
|
||||||
|
opts = opts.username(username);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if let Ok(username) = whoami::username() {
|
||||||
//* case 3: use system username; can't be None
|
|
||||||
debug!("no username was provided, using current username");
|
debug!("no username was provided, using current username");
|
||||||
opts = opts.username(whoami::username());
|
opts = opts.username(username);
|
||||||
}
|
}
|
||||||
// For SSH protocols, only set password if explicitly provided and non-empty.
|
// For SSH protocols, only set password if explicitly provided and non-empty.
|
||||||
// This allows the SSH library to prioritize key-based and agent authentication.
|
// This allows the SSH library to prioritize key-based and agent authentication.
|
||||||
@@ -263,6 +259,17 @@ impl RemoteFsBuilder {
|
|||||||
fn make_ssh_storage(config_client: &ConfigClient) -> SshKeyStorage {
|
fn make_ssh_storage(config_client: &ConfigClient) -> SshKeyStorage {
|
||||||
SshKeyStorage::from(config_client)
|
SshKeyStorage::from(config_client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create tokio runtime to run async code for remotefs
|
||||||
|
fn tokio_runtime() -> Result<Arc<tokio::runtime::Runtime>, String> {
|
||||||
|
Ok(Arc::new(
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.worker_threads(1)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ pub type HostResult<T> = Result<T, HostError>;
|
|||||||
|
|
||||||
/// HostErrorType provides an overview of the specific host error
|
/// HostErrorType provides an overview of the specific host error
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum HostErrorType {
|
pub enum HostErrorType {
|
||||||
#[error("No such file or directory")]
|
#[error("No such file or directory")]
|
||||||
NoSuchFileOrDirectory,
|
NoSuchFileOrDirectory,
|
||||||
@@ -37,6 +36,7 @@ pub enum HostErrorType {
|
|||||||
ExecutionFailed,
|
ExecutionFailed,
|
||||||
#[error("Could not delete file")]
|
#[error("Could not delete file")]
|
||||||
DeleteFailed,
|
DeleteFailed,
|
||||||
|
#[cfg(win)]
|
||||||
#[error("Not implemented")]
|
#[error("Not implemented")]
|
||||||
NotImplemented,
|
NotImplemented,
|
||||||
#[error("remote fs error: {0}")]
|
#[error("remote fs error: {0}")]
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
//! ## Host Bridge
|
||||||
|
//!
|
||||||
|
//! Defines the host abstraction used to expose localhost and bridged remote
|
||||||
|
//! filesystems through a shared interface.
|
||||||
|
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
|||||||
+62
-1
@@ -1,3 +1,8 @@
|
|||||||
|
//! ## Localhost Host Bridge
|
||||||
|
//!
|
||||||
|
//! Implements the host bridge abstraction directly against the local
|
||||||
|
//! filesystem.
|
||||||
|
|
||||||
use std::fs::{self, OpenOptions};
|
use std::fs::{self, OpenOptions};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
@@ -347,6 +352,8 @@ impl HostBridge for Localhost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn exists(&mut self, path: &Path) -> HostResult<bool> {
|
fn exists(&mut self, path: &Path) -> HostResult<bool> {
|
||||||
|
let path: PathBuf = self.to_path(path);
|
||||||
|
|
||||||
Ok(path.exists())
|
Ok(path.exists())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +402,17 @@ impl HostBridge for Localhost {
|
|||||||
fn exec(&mut self, cmd: &str) -> HostResult<String> {
|
fn exec(&mut self, cmd: &str) -> HostResult<String> {
|
||||||
// Make command
|
// Make command
|
||||||
let args: Vec<&str> = cmd.split(' ').collect();
|
let args: Vec<&str> = cmd.split(' ').collect();
|
||||||
let cmd: &str = args.first().unwrap();
|
let cmd: &str = match args.first() {
|
||||||
|
Some(cmd) => cmd,
|
||||||
|
None => {
|
||||||
|
error!("Empty command provided to exec");
|
||||||
|
return Err(HostError::new(
|
||||||
|
HostErrorType::ExecutionFailed,
|
||||||
|
None,
|
||||||
|
self.wrkdir.as_path(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
let argv: &[&str] = &args[1..];
|
let argv: &[&str] = &args[1..];
|
||||||
info!("Executing command: {} {:?}", cmd, argv);
|
info!("Executing command: {} {:?}", cmd, argv);
|
||||||
match std::process::Command::new(cmd).args(argv).output() {
|
match std::process::Command::new(cmd).args(argv).output() {
|
||||||
@@ -698,6 +715,22 @@ mod tests {
|
|||||||
assert!(host.create_file(file.path(), &Metadata::default()).is_err());
|
assert!(host.create_file(file.path(), &Metadata::default()).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(posix)]
|
||||||
|
fn should_resolve_relative_exists_and_stat_from_workdir() {
|
||||||
|
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||||
|
let dir_path: &Path = tmpdir.path();
|
||||||
|
assert!(make_file_at(dir_path, "nested.txt").is_ok());
|
||||||
|
let mut host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
|
||||||
|
|
||||||
|
assert!(host.exists(Path::new("nested.txt")).unwrap());
|
||||||
|
assert!(!host.exists(Path::new("missing.txt")).unwrap());
|
||||||
|
|
||||||
|
let entry = host.stat(Path::new("nested.txt")).unwrap();
|
||||||
|
assert_eq!(entry.path(), &dir_path.join("nested.txt"));
|
||||||
|
assert!(entry.is_file());
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_host_localhost_symlinks() {
|
fn test_host_localhost_symlinks() {
|
||||||
@@ -993,6 +1026,14 @@ mod tests {
|
|||||||
assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5"));
|
assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_host_exec_empty_command() {
|
||||||
|
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||||
|
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||||
|
// Execute empty command should return error
|
||||||
|
assert!(host.exec("").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn should_create_symlink() {
|
fn should_create_symlink() {
|
||||||
@@ -1013,6 +1054,26 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
#[test]
|
||||||
|
fn should_stat_relative_symlink_as_symlink() {
|
||||||
|
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||||
|
let dir_path: &Path = tmpdir.path();
|
||||||
|
assert!(make_file_at(dir_path, "target.txt").is_ok());
|
||||||
|
let mut host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
|
||||||
|
let target = dir_path.join("target.txt");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
host.symlink(Path::new("link.txt"), target.as_path())
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
|
||||||
|
let entry = host.stat(Path::new("link.txt")).unwrap();
|
||||||
|
assert_eq!(entry.path(), &dir_path.join("link.txt"));
|
||||||
|
assert!(entry.is_symlink());
|
||||||
|
assert_eq!(entry.metadata().symlink.as_ref(), Some(&target));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_host_fmt_error() {
|
fn test_host_fmt_error() {
|
||||||
let err: HostError = HostError::new(
|
let err: HostError = HostError::new(
|
||||||
|
|||||||
+106
-2
@@ -1,13 +1,19 @@
|
|||||||
|
//! ## Remote Bridged Host
|
||||||
|
//!
|
||||||
|
//! Bridges a `RemoteFs` implementation behind the local host interface used by
|
||||||
|
//! the file transfer activity.
|
||||||
|
|
||||||
mod temp_mapped_file;
|
mod temp_mapped_file;
|
||||||
|
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
use remotefs::fs::{Metadata, UnixPex};
|
use remotefs::fs::{Metadata, UnixPex};
|
||||||
use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs};
|
use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs};
|
||||||
|
|
||||||
use self::temp_mapped_file::TempMappedFile;
|
use self::temp_mapped_file::TempMappedFile;
|
||||||
use super::{HostBridge, HostError, HostResult};
|
use super::{HostBridge, HostError, HostResult};
|
||||||
|
use crate::utils::path::normalize;
|
||||||
|
|
||||||
struct WriteStreamOp {
|
struct WriteStreamOp {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@@ -124,7 +130,8 @@ impl HostBridge for RemoteBridged {
|
|||||||
|
|
||||||
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
|
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
|
||||||
debug!("Listing directory {:?}", path);
|
debug!("Listing directory {:?}", path);
|
||||||
self.remote.list_dir(path).map_err(HostError::from)
|
let entries = self.remote.list_dir(path).map_err(HostError::from)?;
|
||||||
|
Ok(filter_self_refs(path, entries))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> {
|
fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> {
|
||||||
@@ -209,3 +216,100 @@ impl HostBridge for RemoteBridged {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drop entries that refer to the directory being listed.
|
||||||
|
///
|
||||||
|
/// Some non-compliant FTP servers (e.g. LiteSpeed) include a self-reference
|
||||||
|
/// to the listed directory in the LIST response, which would otherwise appear
|
||||||
|
/// as a duplicate entry in the explorer.
|
||||||
|
fn filter_self_refs(path: &Path, entries: Vec<File>) -> Vec<File> {
|
||||||
|
let normalized = normalize(path);
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.filter(|entry| {
|
||||||
|
let last = entry.path().components().next_back();
|
||||||
|
let is_dot_ref = matches!(last, Some(Component::CurDir | Component::ParentDir));
|
||||||
|
!is_dot_ref && normalize(entry.path()) != normalized
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use remotefs::fs::{FileType, Metadata};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn file(path: &str, file_type: FileType) -> File {
|
||||||
|
File {
|
||||||
|
path: PathBuf::from(path),
|
||||||
|
metadata: Metadata {
|
||||||
|
accessed: Some(SystemTime::UNIX_EPOCH),
|
||||||
|
created: Some(SystemTime::UNIX_EPOCH),
|
||||||
|
modified: Some(SystemTime::UNIX_EPOCH),
|
||||||
|
file_type,
|
||||||
|
gid: None,
|
||||||
|
mode: None,
|
||||||
|
size: 0,
|
||||||
|
symlink: None,
|
||||||
|
uid: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_self_refs_drops_entry_matching_listed_dir() {
|
||||||
|
let entries = vec![
|
||||||
|
file("/wp-content/wp-content", FileType::Directory),
|
||||||
|
file("/wp-content/index.php", FileType::File),
|
||||||
|
];
|
||||||
|
|
||||||
|
let filtered = filter_self_refs(Path::new("/wp-content/wp-content"), entries);
|
||||||
|
|
||||||
|
assert_eq!(filtered.len(), 1);
|
||||||
|
assert_eq!(filtered[0].path(), Path::new("/wp-content/index.php"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_self_refs_drops_dot_and_dotdot_entries() {
|
||||||
|
let entries = vec![
|
||||||
|
file("/foo/.", FileType::Directory),
|
||||||
|
file("/foo/..", FileType::Directory),
|
||||||
|
file("/foo/bar", FileType::File),
|
||||||
|
];
|
||||||
|
|
||||||
|
let filtered = filter_self_refs(Path::new("/foo"), entries);
|
||||||
|
|
||||||
|
assert_eq!(filtered.len(), 1);
|
||||||
|
assert_eq!(filtered[0].path(), Path::new("/foo/bar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_self_refs_normalizes_paths() {
|
||||||
|
let entries = vec![
|
||||||
|
file("/foo/./bar", FileType::File),
|
||||||
|
file("/foo/baz/../", FileType::Directory),
|
||||||
|
];
|
||||||
|
|
||||||
|
let filtered = filter_self_refs(Path::new("/foo"), entries);
|
||||||
|
|
||||||
|
assert_eq!(filtered.len(), 1);
|
||||||
|
assert_eq!(filtered[0].path(), Path::new("/foo/./bar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_self_refs_preserves_unrelated_entries() {
|
||||||
|
let entries = vec![
|
||||||
|
file("/home/user/notes.txt", FileType::File),
|
||||||
|
file("/home/user/photos", FileType::Directory),
|
||||||
|
];
|
||||||
|
|
||||||
|
let filtered = filter_self_refs(Path::new("/home/user"), entries.clone());
|
||||||
|
|
||||||
|
assert_eq!(filtered.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
//! ## Temp Mapped File
|
||||||
|
//!
|
||||||
|
//! Provides a temporary local file that mirrors a remote file while keeping a
|
||||||
|
//! lazily opened read/write handle.
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@@ -16,27 +21,25 @@ pub struct TempMappedFile {
|
|||||||
|
|
||||||
impl Write for TempMappedFile {
|
impl Write for TempMappedFile {
|
||||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
let rc = self.write_hnd()?;
|
let mut handle = self.write_hnd()?;
|
||||||
let mut ref_mut = rc.lock().unwrap();
|
handle.write(buf)
|
||||||
ref_mut.as_mut().unwrap().write(buf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> std::io::Result<()> {
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
let rc = self.write_hnd()?;
|
let mut handle = self.write_hnd()?;
|
||||||
let mut ref_mut = rc.lock().unwrap();
|
handle.flush()
|
||||||
ref_mut.as_mut().unwrap().flush()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Read for TempMappedFile {
|
impl Read for TempMappedFile {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
let rc = self.read_hnd()?;
|
let mut handle = self.read_hnd()?;
|
||||||
let mut ref_mut = rc.lock().unwrap();
|
handle.read(buf)
|
||||||
ref_mut.as_mut().unwrap().read(buf)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TempMappedFile {
|
impl TempMappedFile {
|
||||||
|
/// Creates an empty temporary file container for a downloaded remote file.
|
||||||
pub fn new() -> HostResult<Self> {
|
pub fn new() -> HostResult<Self> {
|
||||||
NamedTempFile::new()
|
NamedTempFile::new()
|
||||||
.map(|tempfile| TempMappedFile {
|
.map(|tempfile| TempMappedFile {
|
||||||
@@ -57,7 +60,13 @@ impl TempMappedFile {
|
|||||||
/// Must be called
|
/// Must be called
|
||||||
pub fn sync(&mut self) -> HostResult<()> {
|
pub fn sync(&mut self) -> HostResult<()> {
|
||||||
{
|
{
|
||||||
let mut lock = self.handle.lock().unwrap();
|
let mut lock = self.lock_handle().map_err(|e| {
|
||||||
|
HostError::new(
|
||||||
|
HostErrorType::FileNotAccessible,
|
||||||
|
Some(e),
|
||||||
|
self.tempfile.path(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
if let Some(hnd) = lock.take() {
|
if let Some(hnd) = lock.take() {
|
||||||
hnd.sync_all().map_err(|e| {
|
hnd.sync_all().map_err(|e| {
|
||||||
@@ -73,28 +82,62 @@ impl TempMappedFile {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_hnd(&mut self) -> io::Result<Arc<Mutex<Option<File>>>> {
|
fn write_hnd(&mut self) -> io::Result<FileHandle<'_>> {
|
||||||
{
|
let mut lock = self.lock_handle()?;
|
||||||
let mut lock = self.handle.lock().unwrap();
|
if lock.is_none() {
|
||||||
if lock.is_none() {
|
let hnd = File::create(self.tempfile.path())?;
|
||||||
let hnd = File::create(self.tempfile.path())?;
|
lock.replace(hnd);
|
||||||
lock.replace(hnd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(self.handle.clone())
|
Ok(FileHandle::new(lock))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_hnd(&mut self) -> io::Result<Arc<Mutex<Option<File>>>> {
|
fn read_hnd(&mut self) -> io::Result<FileHandle<'_>> {
|
||||||
{
|
let mut lock = self.lock_handle()?;
|
||||||
let mut lock = self.handle.lock().unwrap();
|
if lock.is_none() {
|
||||||
if lock.is_none() {
|
let hnd = File::open(self.tempfile.path())?;
|
||||||
let hnd = File::open(self.tempfile.path())?;
|
lock.replace(hnd);
|
||||||
lock.replace(hnd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(self.handle.clone())
|
Ok(FileHandle::new(lock))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lock_handle(&self) -> io::Result<std::sync::MutexGuard<'_, Option<File>>> {
|
||||||
|
self.handle
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| io::Error::other("temporary file handle lock poisoned"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FileHandle<'a> {
|
||||||
|
guard: std::sync::MutexGuard<'a, Option<File>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FileHandle<'a> {
|
||||||
|
fn new(guard: std::sync::MutexGuard<'a, Option<File>>) -> Self {
|
||||||
|
Self { guard }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_mut(&mut self) -> io::Result<&mut File> {
|
||||||
|
self.guard
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| io::Error::other("temporary file handle is not initialized"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for FileHandle<'_> {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.file_mut()?.write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.file_mut()?.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for FileHandle<'_> {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
self.file_mut()?.read(buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -1,3 +1,8 @@
|
|||||||
|
//! ## termscp
|
||||||
|
//!
|
||||||
|
//! Binary entry point for argument parsing, logging setup, and activity
|
||||||
|
//! manager startup.
|
||||||
|
|
||||||
mod activity_manager;
|
mod activity_manager;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
@@ -15,11 +20,7 @@ extern crate bitflags;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_regex;
|
extern crate lazy_regex;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate log;
|
extern crate log;
|
||||||
#[macro_use]
|
|
||||||
extern crate magic_crypt;
|
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
//! ## Import SSH Hosts
|
||||||
|
//!
|
||||||
|
//! Imports OpenSSH host entries into termscp bookmarks and optionally registers
|
||||||
|
//! referenced private keys in the configured SSH key storage.
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -52,7 +57,9 @@ pub fn import_ssh_hosts(ssh_config: Option<PathBuf>, keyring: bool) -> Result<()
|
|||||||
.flat_map(host_to_params)
|
.flat_map(host_to_params)
|
||||||
.for_each(|(name, params, identity_file_params)| {
|
.for_each(|(name, params, identity_file_params)| {
|
||||||
debug!("Adding bookmark for host: {name} with params: {params:?}");
|
debug!("Adding bookmark for host: {name} with params: {params:?}");
|
||||||
bookmarks_client.add_bookmark(name, params, false);
|
if let Err(err) = bookmarks_client.add_bookmark(name, params, false) {
|
||||||
|
error!("Could not add imported bookmark: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
// add ssh key if any
|
// add ssh key if any
|
||||||
if let Some(identity_file_params) = identity_file_params {
|
if let Some(identity_file_params) = identity_file_params {
|
||||||
@@ -255,8 +262,7 @@ mod tests {
|
|||||||
|
|
||||||
struct SshTestConfig {
|
struct SshTestConfig {
|
||||||
config: NamedTempFile,
|
config: NamedTempFile,
|
||||||
#[allow(dead_code)]
|
_identity_file: NamedTempFile,
|
||||||
identity_file: NamedTempFile,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_test_config() -> SshTestConfig {
|
fn ssh_test_config() -> SshTestConfig {
|
||||||
@@ -320,7 +326,7 @@ Host test3
|
|||||||
|
|
||||||
SshTestConfig {
|
SshTestConfig {
|
||||||
config: file,
|
config: file,
|
||||||
identity_file,
|
_identity_file: identity_file,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ pub enum UpdateStatus {
|
|||||||
/// Info related to a github release
|
/// Info related to a github release
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Release {
|
pub struct Release {
|
||||||
|
/// Release version string returned by GitHub.
|
||||||
pub version: String,
|
pub version: String,
|
||||||
|
/// Release notes body returned by GitHub.
|
||||||
pub body: String,
|
pub body: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ impl Update {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Installs the latest available release using the configured update options.
|
||||||
pub fn upgrade(self) -> Result<UpdateStatus, UpdateError> {
|
pub fn upgrade(self) -> Result<UpdateStatus, UpdateError> {
|
||||||
info!("Updating termscp...");
|
info!("Updating termscp...");
|
||||||
GithubUpdater::configure()
|
GithubUpdater::configure()
|
||||||
@@ -132,8 +135,13 @@ impl Update {
|
|||||||
|
|
||||||
/// Check wether new version is higher than new version
|
/// Check wether new version is higher than new version
|
||||||
fn is_new_version_higher(new_version: &str, current_version: &str) -> bool {
|
fn is_new_version_higher(new_version: &str, current_version: &str) -> bool {
|
||||||
version_compare::compare(new_version, current_version).unwrap_or(version_compare::Cmp::Lt)
|
match (
|
||||||
== version_compare::Cmp::Gt
|
semver::Version::parse(new_version),
|
||||||
|
semver::Version::parse(current_version),
|
||||||
|
) {
|
||||||
|
(Ok(new), Ok(current)) => new > current,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<Status> for UpdateStatus {
|
impl From<Status> for UpdateStatus {
|
||||||
@@ -230,15 +238,64 @@ mod test {
|
|||||||
assert_eq!(release.version.as_str(), "0.7.0");
|
assert_eq!(release.version.as_str(), "0.7.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_default_release_body_when_missing() {
|
||||||
|
let release: UpdRelease = UpdRelease {
|
||||||
|
name: String::from("termscp 0.7.0"),
|
||||||
|
version: String::from("0.7.0"),
|
||||||
|
date: String::from("2021-09-12T00:00:00Z"),
|
||||||
|
body: None,
|
||||||
|
assets: vec![],
|
||||||
|
};
|
||||||
|
let release: Release = Release::from(release);
|
||||||
|
|
||||||
|
assert!(release.body.is_empty());
|
||||||
|
assert_eq!(release.version.as_str(), "0.7.0");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_tell_that_version_is_higher() {
|
fn should_tell_that_version_is_higher() {
|
||||||
assert!(Update::is_new_version_higher("0.10.0", "0.9.0"));
|
assert!(Update::is_new_version_higher("0.10.0", "0.9.0"));
|
||||||
assert!(Update::is_new_version_higher("0.20.0", "0.19.0"));
|
assert!(Update::is_new_version_higher("0.20.0", "0.19.1"));
|
||||||
|
assert!(Update::is_new_version_higher("1.0.0", "0.19.1"));
|
||||||
assert!(!Update::is_new_version_higher("0.9.0", "0.10.0"));
|
assert!(!Update::is_new_version_higher("0.9.0", "0.10.0"));
|
||||||
assert!(!Update::is_new_version_higher("0.9.9", "0.10.1"));
|
assert!(!Update::is_new_version_higher("0.9.9", "0.10.1"));
|
||||||
assert!(!Update::is_new_version_higher("0.10.9", "0.11.0"));
|
assert!(!Update::is_new_version_higher("0.10.9", "0.11.0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_ignore_release_without_semver() {
|
||||||
|
let release = Release {
|
||||||
|
version: String::from("latest"),
|
||||||
|
body: String::from("notes"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(Update::check_version(release).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_ignore_release_when_version_is_not_newer() {
|
||||||
|
let release = Release {
|
||||||
|
version: cargo_crate_version!().to_string(),
|
||||||
|
body: String::from("notes"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(Update::check_version(release).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_accept_release_when_version_is_newer() {
|
||||||
|
let release = Release {
|
||||||
|
version: String::from("termscp-999.0.0"),
|
||||||
|
body: String::from("notes"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let release = Update::check_version(release).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(release.version.as_str(), "termscp-999.0.0");
|
||||||
|
assert_eq!(release.body.as_str(), "notes");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_check_whether_github_api_is_reachable() {
|
fn test_should_check_whether_github_api_is_reachable() {
|
||||||
assert!(Update::check_github_api_reachable().is_ok());
|
assert!(Update::check_github_api_reachable().is_ok());
|
||||||
|
|||||||
+252
-139
@@ -2,8 +2,6 @@
|
|||||||
//!
|
//!
|
||||||
//! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system
|
//! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system
|
||||||
|
|
||||||
// Crate
|
|
||||||
// Ext
|
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
@@ -12,11 +10,8 @@ use std::time::SystemTime;
|
|||||||
use super::keys::filestorage::FileStorage;
|
use super::keys::filestorage::FileStorage;
|
||||||
use super::keys::keyringstorage::KeyringStorage;
|
use super::keys::keyringstorage::KeyringStorage;
|
||||||
use super::keys::{KeyStorage, KeyStorageError};
|
use super::keys::{KeyStorage, KeyStorageError};
|
||||||
// Local
|
use crate::config::bookmarks::{Bookmark, UserHosts};
|
||||||
use crate::config::{
|
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
|
||||||
bookmarks::{Bookmark, UserHosts},
|
|
||||||
serialization::{SerializerError, SerializerErrorKind, deserialize, serialize},
|
|
||||||
};
|
|
||||||
use crate::filetransfer::FileTransferParams;
|
use crate::filetransfer::FileTransferParams;
|
||||||
use crate::utils::crypto;
|
use crate::utils::crypto;
|
||||||
use crate::utils::fmt::fmt_time;
|
use crate::utils::fmt::fmt_time;
|
||||||
@@ -104,7 +99,7 @@ impl BookmarksClient {
|
|||||||
fn keyring(storage_path: &Path, keyring: bool) -> (Box<dyn KeyStorage>, &'static str) {
|
fn keyring(storage_path: &Path, keyring: bool) -> (Box<dyn KeyStorage>, &'static str) {
|
||||||
if keyring && cfg!(feature = "keyring") {
|
if keyring && cfg!(feature = "keyring") {
|
||||||
debug!("Setting up KeyStorage");
|
debug!("Setting up KeyStorage");
|
||||||
let username: String = whoami::username();
|
let username = whoami::username().unwrap_or_default();
|
||||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||||
// Check if keyring storage is supported
|
// Check if keyring storage is supported
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
@@ -192,15 +187,15 @@ impl BookmarksClient {
|
|||||||
name: S,
|
name: S,
|
||||||
params: FileTransferParams,
|
params: FileTransferParams,
|
||||||
save_password: bool,
|
save_password: bool,
|
||||||
) {
|
) -> Result<(), SerializerError> {
|
||||||
let name: String = name.as_ref().to_string();
|
let name: String = name.as_ref().to_string();
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
error!("Fatal error; bookmark name is empty");
|
error!("Bookmark name is empty; ignoring add_bookmark request");
|
||||||
panic!("Bookmark name can't be empty");
|
return Ok(());
|
||||||
}
|
}
|
||||||
// Make bookmark
|
// Make bookmark
|
||||||
info!("Added bookmark {}", name);
|
info!("Added bookmark {}", name);
|
||||||
let mut host: Bookmark = self.make_bookmark(params);
|
let mut host: Bookmark = self.make_bookmark(params)?;
|
||||||
// If not save_password, set secrets to `None`
|
// If not save_password, set secrets to `None`
|
||||||
if !save_password {
|
if !save_password {
|
||||||
host.password = None;
|
host.password = None;
|
||||||
@@ -210,6 +205,7 @@ impl BookmarksClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.hosts.bookmarks.insert(name, host);
|
self.hosts.bookmarks.insert(name, host);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete entry from bookmarks
|
/// Delete entry from bookmarks
|
||||||
@@ -231,9 +227,9 @@ impl BookmarksClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new recent to bookmarks
|
/// Add a new recent to bookmarks
|
||||||
pub fn add_recent(&mut self, params: FileTransferParams) {
|
pub fn add_recent(&mut self, params: FileTransferParams) -> Result<(), SerializerError> {
|
||||||
// Make bookmark
|
// Make bookmark
|
||||||
let mut host: Bookmark = self.make_bookmark(params);
|
let mut host: Bookmark = self.make_bookmark(params)?;
|
||||||
// Null password for recents
|
// Null password for recents
|
||||||
host.password = None;
|
host.password = None;
|
||||||
if let Some(s3) = host.s3.as_mut() {
|
if let Some(s3) = host.s3.as_mut() {
|
||||||
@@ -245,7 +241,7 @@ impl BookmarksClient {
|
|||||||
if *value == host {
|
if *value == host {
|
||||||
debug!("Discarding recent since duplicated ({})", key);
|
debug!("Discarding recent since duplicated ({})", key);
|
||||||
// Don't save duplicates
|
// Don't save duplicates
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If hosts size is bigger than self.recents_size; pop last
|
// If hosts size is bigger than self.recents_size; pop last
|
||||||
@@ -270,6 +266,7 @@ impl BookmarksClient {
|
|||||||
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
||||||
info!("Saved recent host {}", name);
|
info!("Saved recent host {}", name);
|
||||||
self.hosts.recents.insert(name, host);
|
self.hosts.recents.insert(name, host);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete entry from recents
|
/// Delete entry from recents
|
||||||
@@ -334,27 +331,29 @@ impl BookmarksClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Make bookmark from credentials
|
/// Make bookmark from credentials
|
||||||
fn make_bookmark(&self, params: FileTransferParams) -> Bookmark {
|
fn make_bookmark(&self, params: FileTransferParams) -> Result<Bookmark, SerializerError> {
|
||||||
let mut bookmark: Bookmark = Bookmark::from(params);
|
let mut bookmark: Bookmark = Bookmark::from(params);
|
||||||
// Encrypt password
|
// Encrypt password
|
||||||
if let Some(pwd) = bookmark.password {
|
if let Some(pwd) = bookmark.password {
|
||||||
bookmark.password = Some(self.encrypt_str(pwd.as_str()));
|
bookmark.password = Some(self.encrypt_str(pwd.as_str())?);
|
||||||
}
|
}
|
||||||
// Encrypt aws s3 params
|
// Encrypt aws s3 params
|
||||||
if let Some(s3) = bookmark.s3.as_mut() {
|
if let Some(s3) = bookmark.s3.as_mut() {
|
||||||
if let Some(access_key) = s3.access_key.as_mut() {
|
if let Some(access_key) = s3.access_key.as_mut() {
|
||||||
*access_key = self.encrypt_str(access_key.as_str());
|
*access_key = self.encrypt_str(access_key.as_str())?;
|
||||||
}
|
}
|
||||||
if let Some(secret_access_key) = s3.secret_access_key.as_mut() {
|
if let Some(secret_access_key) = s3.secret_access_key.as_mut() {
|
||||||
*secret_access_key = self.encrypt_str(secret_access_key.as_str());
|
*secret_access_key = self.encrypt_str(secret_access_key.as_str())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bookmark
|
Ok(bookmark)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt provided string using AES-128. Encrypted buffer is then converted to BASE64
|
/// Encrypt provided string using AES-128. Encrypted buffer is then converted to BASE64
|
||||||
fn encrypt_str(&self, txt: &str) -> String {
|
fn encrypt_str(&self, txt: &str) -> Result<String, SerializerError> {
|
||||||
crypto::aes128_b64_crypt(self.key.as_str(), txt)
|
crypto::aes128_b64_crypt(self.key.as_str(), txt).map_err(|err| {
|
||||||
|
SerializerError::new_ex(SerializerErrorKind::Serialization, err.to_string())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt provided string using AES-128
|
/// Decrypt provided string using AES-128
|
||||||
@@ -408,24 +407,32 @@ mod tests {
|
|||||||
let mut client: BookmarksClient =
|
let mut client: BookmarksClient =
|
||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
// Add some bookmarks
|
// Add some bookmarks
|
||||||
client.add_bookmark(
|
assert!(
|
||||||
"raspberry",
|
client
|
||||||
make_generic_ftparams(
|
.add_bookmark(
|
||||||
FileTransferProtocol::Sftp,
|
"raspberry",
|
||||||
"192.168.1.31",
|
make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.31",
|
||||||
Some("mypassword"),
|
22,
|
||||||
),
|
"pi",
|
||||||
true,
|
Some("mypassword"),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
client
|
||||||
|
.add_recent(make_generic_ftparams(
|
||||||
|
FileTransferProtocol::Sftp,
|
||||||
|
"192.168.1.31",
|
||||||
|
22,
|
||||||
|
"pi",
|
||||||
|
Some("mypassword"),
|
||||||
|
))
|
||||||
|
.is_ok()
|
||||||
);
|
);
|
||||||
client.add_recent(make_generic_ftparams(
|
|
||||||
FileTransferProtocol::Sftp,
|
|
||||||
"192.168.1.31",
|
|
||||||
22,
|
|
||||||
"pi",
|
|
||||||
Some("mypassword"),
|
|
||||||
));
|
|
||||||
let recent_key: String = String::from(client.iter_recents().next().unwrap());
|
let recent_key: String = String::from(client.iter_recents().next().unwrap());
|
||||||
assert!(client.write_bookmarks().is_ok());
|
assert!(client.write_bookmarks().is_ok());
|
||||||
let key: String = client.key.clone();
|
let key: String = client.key.clone();
|
||||||
@@ -456,7 +463,11 @@ mod tests {
|
|||||||
let mut client: BookmarksClient =
|
let mut client: BookmarksClient =
|
||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
// Add s3 bookmark
|
// Add s3 bookmark
|
||||||
client.add_bookmark("my-bucket", make_s3_ftparams(), true);
|
assert!(
|
||||||
|
client
|
||||||
|
.add_bookmark("my-bucket", make_s3_ftparams(), true)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// Verify bookmark
|
// Verify bookmark
|
||||||
let bookmark = client.get_bookmark("my-bucket").unwrap();
|
let bookmark = client.get_bookmark("my-bucket").unwrap();
|
||||||
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
||||||
@@ -476,7 +487,11 @@ mod tests {
|
|||||||
let mut client: BookmarksClient =
|
let mut client: BookmarksClient =
|
||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
// Add s3 bookmark
|
// Add s3 bookmark
|
||||||
client.add_bookmark("my-bucket", make_s3_ftparams(), false);
|
assert!(
|
||||||
|
client
|
||||||
|
.add_bookmark("my-bucket", make_s3_ftparams(), false)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// Verify bookmark
|
// Verify bookmark
|
||||||
let bookmark = client.get_bookmark("my-bucket").unwrap();
|
let bookmark = client.get_bookmark("my-bucket").unwrap();
|
||||||
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
||||||
@@ -497,7 +512,7 @@ mod tests {
|
|||||||
let mut client: BookmarksClient =
|
let mut client: BookmarksClient =
|
||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
// Add s3 bookmark
|
// Add s3 bookmark
|
||||||
client.add_recent(make_s3_ftparams());
|
assert!(client.add_recent(make_s3_ftparams()).is_ok());
|
||||||
// Verify bookmark
|
// Verify bookmark
|
||||||
let bookmark = client.iter_recents().next().unwrap();
|
let bookmark = client.iter_recents().next().unwrap();
|
||||||
let bookmark = client.get_recent(bookmark).unwrap();
|
let bookmark = client.get_recent(bookmark).unwrap();
|
||||||
@@ -520,27 +535,35 @@ mod tests {
|
|||||||
let mut client: BookmarksClient =
|
let mut client: BookmarksClient =
|
||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
// Add bookmark
|
// Add bookmark
|
||||||
client.add_bookmark(
|
assert!(
|
||||||
"raspberry",
|
client
|
||||||
make_generic_ftparams(
|
.add_bookmark(
|
||||||
FileTransferProtocol::Sftp,
|
"raspberry",
|
||||||
"192.168.1.31",
|
make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.31",
|
||||||
Some("mypassword"),
|
22,
|
||||||
),
|
"pi",
|
||||||
true,
|
Some("mypassword"),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
);
|
);
|
||||||
client.add_bookmark(
|
assert!(
|
||||||
"raspberry2",
|
client
|
||||||
make_generic_ftparams(
|
.add_bookmark(
|
||||||
FileTransferProtocol::Sftp,
|
"raspberry2",
|
||||||
"192.168.1.31",
|
make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.31",
|
||||||
Some("mypassword2"),
|
22,
|
||||||
),
|
"pi",
|
||||||
true,
|
Some("mypassword2"),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
);
|
);
|
||||||
// Iter
|
// Iter
|
||||||
assert_eq!(client.iter_bookmarks().count(), 2);
|
assert_eq!(client.iter_bookmarks().count(), 2);
|
||||||
@@ -562,26 +585,30 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic]
|
|
||||||
|
|
||||||
fn test_system_bookmarks_bad_bookmark_name() {
|
fn test_system_bookmarks_bad_bookmark_name() {
|
||||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
let mut client: BookmarksClient =
|
||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
// Add bookmark
|
// Add bookmark with empty name should be silently ignored
|
||||||
client.add_bookmark(
|
assert!(
|
||||||
"",
|
client
|
||||||
make_generic_ftparams(
|
.add_bookmark(
|
||||||
FileTransferProtocol::Sftp,
|
"",
|
||||||
"192.168.1.31",
|
make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.31",
|
||||||
Some("mypassword"),
|
22,
|
||||||
),
|
"pi",
|
||||||
true,
|
Some("mypassword"),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
);
|
);
|
||||||
|
// No bookmark should have been added
|
||||||
|
assert_eq!(client.iter_bookmarks().count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -592,16 +619,20 @@ mod tests {
|
|||||||
let mut client: BookmarksClient =
|
let mut client: BookmarksClient =
|
||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
// Add bookmark
|
// Add bookmark
|
||||||
client.add_bookmark(
|
assert!(
|
||||||
"raspberry",
|
client
|
||||||
make_generic_ftparams(
|
.add_bookmark(
|
||||||
FileTransferProtocol::Sftp,
|
"raspberry",
|
||||||
"192.168.1.31",
|
make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.31",
|
||||||
Some("mypassword"),
|
22,
|
||||||
),
|
"pi",
|
||||||
false,
|
Some("mypassword"),
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
);
|
);
|
||||||
let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
|
let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
|
||||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||||
@@ -620,13 +651,17 @@ mod tests {
|
|||||||
let mut client: BookmarksClient =
|
let mut client: BookmarksClient =
|
||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
// Add bookmark
|
// Add bookmark
|
||||||
client.add_recent(make_generic_ftparams(
|
assert!(
|
||||||
FileTransferProtocol::Sftp,
|
client
|
||||||
"192.168.1.31",
|
.add_recent(make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.31",
|
||||||
Some("mypassword"),
|
22,
|
||||||
));
|
"pi",
|
||||||
|
Some("mypassword"),
|
||||||
|
))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// Iter
|
// Iter
|
||||||
assert_eq!(client.iter_recents().count(), 1);
|
assert_eq!(client.iter_recents().count(), 1);
|
||||||
let key: String = String::from(client.iter_recents().next().unwrap());
|
let key: String = String::from(client.iter_recents().next().unwrap());
|
||||||
@@ -656,20 +691,28 @@ mod tests {
|
|||||||
let mut client: BookmarksClient =
|
let mut client: BookmarksClient =
|
||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
// Add bookmark
|
// Add bookmark
|
||||||
client.add_recent(make_generic_ftparams(
|
assert!(
|
||||||
FileTransferProtocol::Sftp,
|
client
|
||||||
"192.168.1.31",
|
.add_recent(make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.31",
|
||||||
Some("mypassword"),
|
22,
|
||||||
));
|
"pi",
|
||||||
client.add_recent(make_generic_ftparams(
|
Some("mypassword"),
|
||||||
FileTransferProtocol::Sftp,
|
))
|
||||||
"192.168.1.31",
|
.is_ok()
|
||||||
22,
|
);
|
||||||
"pi",
|
assert!(
|
||||||
Some("mypassword"),
|
client
|
||||||
));
|
.add_recent(make_generic_ftparams(
|
||||||
|
FileTransferProtocol::Sftp,
|
||||||
|
"192.168.1.31",
|
||||||
|
22,
|
||||||
|
"pi",
|
||||||
|
Some("mypassword"),
|
||||||
|
))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// There should be only one recent
|
// There should be only one recent
|
||||||
assert_eq!(client.iter_recents().count(), 1);
|
assert_eq!(client.iter_recents().count(), 1);
|
||||||
}
|
}
|
||||||
@@ -684,31 +727,43 @@ mod tests {
|
|||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2, true).unwrap();
|
||||||
// Add recent, wait 1 second for each one (cause the name depends on time)
|
// Add recent, wait 1 second for each one (cause the name depends on time)
|
||||||
// 1
|
// 1
|
||||||
client.add_recent(make_generic_ftparams(
|
assert!(
|
||||||
FileTransferProtocol::Sftp,
|
client
|
||||||
"192.168.1.1",
|
.add_recent(make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.1",
|
||||||
Some("mypassword"),
|
22,
|
||||||
));
|
"pi",
|
||||||
|
Some("mypassword"),
|
||||||
|
))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
sleep(Duration::from_secs(1));
|
sleep(Duration::from_secs(1));
|
||||||
// 2
|
// 2
|
||||||
client.add_recent(make_generic_ftparams(
|
assert!(
|
||||||
FileTransferProtocol::Sftp,
|
client
|
||||||
"192.168.1.2",
|
.add_recent(make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.2",
|
||||||
Some("mypassword"),
|
22,
|
||||||
));
|
"pi",
|
||||||
|
Some("mypassword"),
|
||||||
|
))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
sleep(Duration::from_secs(1));
|
sleep(Duration::from_secs(1));
|
||||||
// 3
|
// 3
|
||||||
client.add_recent(make_generic_ftparams(
|
assert!(
|
||||||
FileTransferProtocol::Sftp,
|
client
|
||||||
"192.168.1.3",
|
.add_recent(make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.3",
|
||||||
Some("mypassword"),
|
22,
|
||||||
));
|
"pi",
|
||||||
|
Some("mypassword"),
|
||||||
|
))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// Limit is 2
|
// Limit is 2
|
||||||
assert_eq!(client.iter_recents().count(), 2);
|
assert_eq!(client.iter_recents().count(), 2);
|
||||||
// Check that 192.168.1.1 has been removed
|
// Check that 192.168.1.1 has been removed
|
||||||
@@ -743,25 +798,30 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic]
|
|
||||||
fn test_system_bookmarks_add_bookmark_empty() {
|
fn test_system_bookmarks_add_bookmark_empty() {
|
||||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
let mut client: BookmarksClient =
|
||||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
// Add bookmark
|
// Add bookmark with empty name should be silently ignored
|
||||||
client.add_bookmark(
|
assert!(
|
||||||
"",
|
client
|
||||||
make_generic_ftparams(
|
.add_bookmark(
|
||||||
FileTransferProtocol::Sftp,
|
"",
|
||||||
"192.168.1.31",
|
make_generic_ftparams(
|
||||||
22,
|
FileTransferProtocol::Sftp,
|
||||||
"pi",
|
"192.168.1.31",
|
||||||
Some("mypassword"),
|
22,
|
||||||
),
|
"pi",
|
||||||
true,
|
Some("mypassword"),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
);
|
);
|
||||||
|
// No bookmark should have been added
|
||||||
|
assert_eq!(client.iter_bookmarks().count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -779,6 +839,59 @@ mod tests {
|
|||||||
assert!(client.decrypt_str("bidoof").is_err());
|
assert!(client.decrypt_str("bidoof").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_return_bookmark_when_password_decryption_fails() {
|
||||||
|
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||||
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
|
let mut client =
|
||||||
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
|
let mut bookmark = Bookmark::from(make_generic_ftparams(
|
||||||
|
FileTransferProtocol::Sftp,
|
||||||
|
"192.168.1.31",
|
||||||
|
22,
|
||||||
|
"pi",
|
||||||
|
Some("mypassword"),
|
||||||
|
));
|
||||||
|
bookmark.password = Some(String::from("not-valid-base64"));
|
||||||
|
client
|
||||||
|
.hosts
|
||||||
|
.bookmarks
|
||||||
|
.insert(String::from("raspberry"), bookmark);
|
||||||
|
|
||||||
|
let bookmark = ftparams_to_tup(client.get_bookmark("raspberry").unwrap());
|
||||||
|
|
||||||
|
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||||
|
assert_eq!(bookmark.1, 22);
|
||||||
|
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||||
|
assert_eq!(bookmark.3, String::from("pi"));
|
||||||
|
assert_eq!(bookmark.4.as_deref(), Some("not-valid-base64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_return_s3_bookmark_when_secret_decryption_fails() {
|
||||||
|
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||||
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
|
let mut client =
|
||||||
|
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||||
|
let mut bookmark = Bookmark::from(make_s3_ftparams());
|
||||||
|
let s3 = bookmark.s3.as_mut().unwrap();
|
||||||
|
s3.access_key = Some(String::from("bad-access-key"));
|
||||||
|
s3.secret_access_key = Some(String::from("bad-secret-key"));
|
||||||
|
client
|
||||||
|
.hosts
|
||||||
|
.bookmarks
|
||||||
|
.insert(String::from("my-bucket"), bookmark);
|
||||||
|
|
||||||
|
let bookmark = client.get_bookmark("my-bucket").unwrap();
|
||||||
|
let params = bookmark.params.s3_params().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
||||||
|
assert_eq!(params.bucket_name.as_str(), "omar");
|
||||||
|
assert_eq!(params.region.as_deref(), Some("eu-west-1"));
|
||||||
|
assert_eq!(params.access_key.as_deref(), Some("bad-access-key"));
|
||||||
|
assert_eq!(params.secret_access_key.as_deref(), Some("bad-secret-key"));
|
||||||
|
}
|
||||||
|
|
||||||
/// Get paths for configuration and key for bookmarks
|
/// Get paths for configuration and key for bookmarks
|
||||||
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||||
let k: PathBuf = PathBuf::from(dir);
|
let k: PathBuf = PathBuf::from(dir);
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ impl ConfigClient {
|
|||||||
None => None,
|
None => None,
|
||||||
Some(key_path) => {
|
Some(key_path) => {
|
||||||
// Get host and username
|
// Get host and username
|
||||||
let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
|
let (host, username) = Self::get_ssh_tokens(mkey)?;
|
||||||
// Return key
|
// Return key
|
||||||
Some((host, username, PathBuf::from(key_path)))
|
Some((host, username, PathBuf::from(key_path)))
|
||||||
}
|
}
|
||||||
@@ -389,12 +389,18 @@ impl ConfigClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get ssh tokens starting from ssh host key
|
/// Get ssh tokens starting from ssh host key
|
||||||
/// Panics if key has invalid syntax
|
/// Returns: (host, username) or None if key has invalid syntax
|
||||||
/// Returns: (host, username)
|
fn get_ssh_tokens(host_key: &str) -> Option<(String, String)> {
|
||||||
fn get_ssh_tokens(host_key: &str) -> (String, String) {
|
|
||||||
let tokens: Vec<&str> = host_key.split('@').collect();
|
let tokens: Vec<&str> = host_key.split('@').collect();
|
||||||
assert!(tokens.len() >= 2);
|
if tokens.len() >= 2 {
|
||||||
(String::from(tokens[1]), String::from(tokens[0]))
|
Some((String::from(tokens[1]), String::from(tokens[0])))
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"Invalid SSH host key format: '{}' (expected 'username@host')",
|
||||||
|
host_key
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make serializer error from `std::io::Error`
|
/// Make serializer error from `std::io::Error`
|
||||||
@@ -711,10 +717,16 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ConfigClient::get_ssh_tokens("pi@192.168.1.31"),
|
ConfigClient::get_ssh_tokens("pi@192.168.1.31"),
|
||||||
(String::from("192.168.1.31"), String::from("pi"))
|
Some((String::from("192.168.1.31"), String::from("pi")))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_config_get_ssh_tokens_invalid() {
|
||||||
|
assert!(ConfigClient::get_ssh_tokens("invalid").is_none());
|
||||||
|
assert!(ConfigClient::get_ssh_tokens("").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_system_config_make_io_err() {
|
fn test_system_config_make_io_err() {
|
||||||
let err: SerializerError =
|
let err: SerializerError =
|
||||||
|
|||||||
+11
-20
@@ -4,20 +4,21 @@
|
|||||||
|
|
||||||
// Ext
|
// Ext
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
static CONF_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::config_dir);
|
||||||
|
#[cfg(test)]
|
||||||
|
static CONF_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(|| Some(std::env::temp_dir()));
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
static CACHE_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::cache_dir);
|
||||||
|
#[cfg(test)]
|
||||||
|
static CACHE_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(|| Some(std::env::temp_dir()));
|
||||||
|
|
||||||
/// Get termscp config directory path and initialize it.
|
/// Get termscp config directory path and initialize it.
|
||||||
/// Returns None if it's not possible to initialize it
|
/// Returns None if it's not possible to initialize it
|
||||||
pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
|
pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
|
||||||
// Get file
|
|
||||||
#[cfg(not(test))]
|
|
||||||
lazy_static! {
|
|
||||||
static ref CONF_DIR: Option<PathBuf> = dirs::config_dir();
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
|
||||||
lazy_static! {
|
|
||||||
static ref CONF_DIR: Option<PathBuf> = Some(std::env::temp_dir());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(dir) = CONF_DIR.as_deref() {
|
if let Some(dir) = CONF_DIR.as_deref() {
|
||||||
init_dir(dir).map(Option::Some)
|
init_dir(dir).map(Option::Some)
|
||||||
} else {
|
} else {
|
||||||
@@ -28,16 +29,6 @@ pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
|
|||||||
/// Get termscp cache directory path and initialize it.
|
/// Get termscp cache directory path and initialize it.
|
||||||
/// Returns None if it's not possible to initialize it
|
/// Returns None if it's not possible to initialize it
|
||||||
pub fn init_cache_dir() -> Result<Option<PathBuf>, String> {
|
pub fn init_cache_dir() -> Result<Option<PathBuf>, String> {
|
||||||
// Get file
|
|
||||||
#[cfg(not(test))]
|
|
||||||
lazy_static! {
|
|
||||||
static ref CACHE_DIR: Option<PathBuf> = dirs::cache_dir();
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
|
||||||
lazy_static! {
|
|
||||||
static ref CACHE_DIR: Option<PathBuf> = Some(std::env::temp_dir());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(dir) = CACHE_DIR.as_deref() {
|
if let Some(dir) = CACHE_DIR.as_deref() {
|
||||||
init_dir(dir).map(Option::Some)
|
init_dir(dir).map(Option::Some)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use thiserror::Error;
|
|||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum KeyStorageError {
|
pub enum KeyStorageError {
|
||||||
#[error("Key has a bad syntax")]
|
#[error("Key has a bad syntax")]
|
||||||
BadSytax,
|
BadSyntax,
|
||||||
#[error("Provider service error")]
|
#[error("Provider service error")]
|
||||||
ProviderError,
|
ProviderError,
|
||||||
#[error("No such key")]
|
#[error("No such key")]
|
||||||
@@ -54,7 +54,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_system_keys_mod_errors() {
|
fn test_system_keys_mod_errors() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
KeyStorageError::BadSytax.to_string(),
|
KeyStorageError::BadSyntax.to_string(),
|
||||||
String::from("Key has a bad syntax")
|
String::from("Key has a bad syntax")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -71,7 +71,10 @@ impl KeyStorage for FileStorage {
|
|||||||
return Err(KeyStorageError::ProviderError);
|
return Err(KeyStorageError::ProviderError);
|
||||||
}
|
}
|
||||||
// Set file to readonly
|
// Set file to readonly
|
||||||
let mut permissions: Permissions = file.metadata().unwrap().permissions();
|
let mut permissions: Permissions = file
|
||||||
|
.metadata()
|
||||||
|
.map_err(|_| KeyStorageError::ProviderError)?
|
||||||
|
.permissions();
|
||||||
permissions.set_readonly(true);
|
permissions.set_readonly(true);
|
||||||
let _ = file.set_permissions(permissions);
|
let _ = file.set_permissions(permissions);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ impl KeyStorage for KeyringStorage {
|
|||||||
| KeyringError::Invalid(_, _)
|
| KeyringError::Invalid(_, _)
|
||||||
| KeyringError::Ambiguous(_) => Err(KeyStorageError::ProviderError),
|
| KeyringError::Ambiguous(_) => Err(KeyStorageError::ProviderError),
|
||||||
KeyringError::BadEncoding(_) | KeyringError::TooLong(_, _) => {
|
KeyringError::BadEncoding(_) | KeyringError::TooLong(_, _) => {
|
||||||
Err(KeyStorageError::BadSytax)
|
Err(KeyStorageError::BadSyntax)
|
||||||
}
|
}
|
||||||
_ => Err(KeyStorageError::ProviderError),
|
_ => Err(KeyStorageError::ProviderError),
|
||||||
},
|
},
|
||||||
@@ -78,14 +78,13 @@ impl KeyStorage for KeyringStorage {
|
|||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))]
|
#[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))]
|
||||||
fn test_system_keys_keyringstorage() {
|
fn test_system_keys_keyring_storage() {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use whoami::username;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
let username: String = username();
|
let username = whoami::username().expect("no username");
|
||||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
let storage = KeyringStorage::new(username.as_str());
|
||||||
assert!(storage.is_supported());
|
assert!(storage.is_supported());
|
||||||
let app_name: &str = "termscp-test2";
|
let app_name: &str = "termscp-test2";
|
||||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ use ssh2_config::SshConfig;
|
|||||||
use super::config_client::ConfigClient;
|
use super::config_client::ConfigClient;
|
||||||
use crate::utils::ssh as ssh_utils;
|
use crate::utils::ssh as ssh_utils;
|
||||||
|
|
||||||
|
/// Resolves SSH identity files from termscp config, SSH config, and standard
|
||||||
|
/// OpenSSH default locations.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct SshKeyStorage {
|
pub struct SshKeyStorage {
|
||||||
/// Association between {user}@{host} and RSA key path
|
/// Association between {user}@{host} and RSA key path
|
||||||
@@ -38,7 +40,7 @@ impl SshKeyStorage {
|
|||||||
/// Resolve host via termscp ssh keys storage
|
/// Resolve host via termscp ssh keys storage
|
||||||
fn resolve_host_in_termscp_storage(&self, host: &str, username: &str) -> Option<&Path> {
|
fn resolve_host_in_termscp_storage(&self, host: &str, username: &str) -> Option<&Path> {
|
||||||
let key: String = Self::make_mapkey(host, username);
|
let key: String = Self::make_mapkey(host, username);
|
||||||
self.hosts.get(&key).map(|x| x.as_path())
|
self.hosts.get(&key).map(PathBuf::as_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve host via ssh2 configuration
|
/// Resolve host via ssh2 configuration
|
||||||
@@ -206,6 +208,43 @@ Host test
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_make_mapkey_from_username_and_host() {
|
||||||
|
assert_eq!(
|
||||||
|
SshKeyStorage::make_mapkey("example.org", "veeso"),
|
||||||
|
"veeso@example.org"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_prefer_termscp_key_over_ssh_config() {
|
||||||
|
let rsa_key = test_helpers::create_sample_file_with_content(
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a",
|
||||||
|
);
|
||||||
|
let ssh_config_file = test_helpers::create_sample_file_with_content(format!(
|
||||||
|
r#"
|
||||||
|
Host test
|
||||||
|
HostName 127.0.0.1
|
||||||
|
User test
|
||||||
|
IdentityFile {}
|
||||||
|
"#,
|
||||||
|
rsa_key.path().display()
|
||||||
|
));
|
||||||
|
let tmp_dir: tempfile::TempDir = tempfile::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();
|
||||||
|
client.set_ssh_config(Some(ssh_config_file.path().to_string_lossy().to_string()));
|
||||||
|
assert!(client.add_ssh_key("test", "pi", "stored-key").is_ok());
|
||||||
|
|
||||||
|
let storage: SshKeyStorage = SshKeyStorage::from(&client);
|
||||||
|
let resolved = storage.resolve("test", "pi").unwrap();
|
||||||
|
|
||||||
|
assert!(resolved.ends_with("pi@test.key"));
|
||||||
|
assert_ne!(resolved.as_path(), rsa_key.path());
|
||||||
|
}
|
||||||
|
|
||||||
/// Get paths for configuration and keys directory
|
/// Get paths for configuration and keys directory
|
||||||
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||||
let mut k: PathBuf = PathBuf::from(dir);
|
let mut k: PathBuf = PathBuf::from(dir);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ impl ThemeProvider {
|
|||||||
pub fn new(theme_path: &Path) -> Result<Self, SerializerError> {
|
pub fn new(theme_path: &Path) -> Result<Self, SerializerError> {
|
||||||
let default_theme: Theme = Theme::default();
|
let default_theme: Theme = Theme::default();
|
||||||
info!(
|
info!(
|
||||||
"Setting up theme provider with thene path {} ",
|
"Setting up theme provider with theme path {}",
|
||||||
theme_path.display(),
|
theme_path.display(),
|
||||||
);
|
);
|
||||||
// Create provider
|
// Create provider
|
||||||
@@ -42,7 +42,7 @@ impl ThemeProvider {
|
|||||||
} else {
|
} else {
|
||||||
// otherwise Load configuration from file
|
// otherwise Load configuration from file
|
||||||
if let Err(err) = provider.load() {
|
if let Err(err) = provider.load() {
|
||||||
error!("Couldn't read thene file: {}", err);
|
error!("Couldn't read theme file: {}", err);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
debug!("Read theme file");
|
debug!("Read theme file");
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ pub enum FsWatcherError {
|
|||||||
PathNotWatched,
|
PathNotWatched,
|
||||||
#[error("unable to watch path, since it's already watched")]
|
#[error("unable to watch path, since it's already watched")]
|
||||||
PathAlreadyWatched,
|
PathAlreadyWatched,
|
||||||
|
#[error("watcher event channel disconnected")]
|
||||||
|
Disconnected,
|
||||||
#[error("unknown event: {0}")]
|
#[error("unknown event: {0}")]
|
||||||
UnknownEvent(&'static str),
|
UnknownEvent(&'static str),
|
||||||
#[error("worker error: {0}")]
|
#[error("worker error: {0}")]
|
||||||
@@ -114,7 +116,7 @@ impl FsWatcher {
|
|||||||
let res = match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
let res = match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(RecvTimeoutError::Timeout) => return Ok(None),
|
Err(RecvTimeoutError::Timeout) => return Ok(None),
|
||||||
Err(RecvTimeoutError::Disconnected) => panic!("File watcher died"),
|
Err(RecvTimeoutError::Disconnected) => return Err(FsWatcherError::Disconnected),
|
||||||
};
|
};
|
||||||
|
|
||||||
// convert event to FsChange
|
// convert event to FsChange
|
||||||
@@ -158,7 +160,7 @@ impl FsWatcher {
|
|||||||
|
|
||||||
/// Returns the list of watched paths
|
/// Returns the list of watched paths
|
||||||
pub fn watched_paths(&self) -> Vec<&Path> {
|
pub fn watched_paths(&self) -> Vec<&Path> {
|
||||||
Vec::from_iter(self.paths.keys().map(|x| x.as_path()))
|
Vec::from_iter(self.paths.keys().map(PathBuf::as_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unwatch provided path.
|
/// Unwatch provided path.
|
||||||
@@ -13,9 +13,10 @@ mod view;
|
|||||||
// Includes
|
// Includes
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tuirealm::application::PollStrategy;
|
use tuirealm::application::{Application, PollStrategy};
|
||||||
|
use tuirealm::event::NoUserEvent;
|
||||||
use tuirealm::listener::EventListenerCfg;
|
use tuirealm::listener::EventListenerCfg;
|
||||||
use tuirealm::{Application, NoUserEvent, Update};
|
use tuirealm::terminal::TerminalAdapter;
|
||||||
|
|
||||||
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
|
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
|
||||||
use crate::config::themes::Theme;
|
use crate::config::themes::Theme;
|
||||||
@@ -254,9 +255,7 @@ impl AuthActivity {
|
|||||||
pub fn new(ticks: Duration) -> AuthActivity {
|
pub fn new(ticks: Duration) -> AuthActivity {
|
||||||
AuthActivity {
|
AuthActivity {
|
||||||
app: Application::init(
|
app: Application::init(
|
||||||
EventListenerCfg::default()
|
EventListenerCfg::default().crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
||||||
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL)
|
|
||||||
.poll_timeout(ticks),
|
|
||||||
),
|
),
|
||||||
context: None,
|
context: None,
|
||||||
bookmarks_list: Vec::new(),
|
bookmarks_list: Vec::new(),
|
||||||
@@ -369,7 +368,10 @@ impl Activity for AuthActivity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Tick
|
// Tick
|
||||||
match self.app.tick(PollStrategy::UpTo(3)) {
|
match self
|
||||||
|
.app
|
||||||
|
.tick(PollStrategy::UpTo(3, std::time::Duration::from_millis(10)))
|
||||||
|
{
|
||||||
Ok(messages) => {
|
Ok(messages) => {
|
||||||
for msg in messages.into_iter() {
|
for msg in messages.into_iter() {
|
||||||
let mut msg = Some(msg);
|
let mut msg = Some(msg);
|
||||||
@@ -71,7 +71,10 @@ impl AuthActivity {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(bookmarks_cli) = self.bookmarks_client_mut() {
|
if let Some(bookmarks_cli) = self.bookmarks_client_mut() {
|
||||||
bookmarks_cli.add_bookmark(name.clone(), params, save_password);
|
if let Err(err) = bookmarks_cli.add_bookmark(name.clone(), params, save_password) {
|
||||||
|
self.mount_error(format!("Could not save bookmark: {err}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Save bookmarks
|
// Save bookmarks
|
||||||
self.write_bookmarks();
|
self.write_bookmarks();
|
||||||
// Remove `name` from bookmarks if exists
|
// Remove `name` from bookmarks if exists
|
||||||
@@ -121,7 +124,10 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(bookmarks_cli) = self.bookmarks_client_mut() {
|
if let Some(bookmarks_cli) = self.bookmarks_client_mut() {
|
||||||
bookmarks_cli.add_recent(params);
|
if let Err(err) = bookmarks_cli.add_recent(params) {
|
||||||
|
self.mount_error(format!("Could not save recent host: {err}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Save bookmarks
|
// Save bookmarks
|
||||||
self.write_bookmarks();
|
self.write_bookmarks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,19 +28,19 @@ pub use popup::{
|
|||||||
WindowSizeError,
|
WindowSizeError,
|
||||||
};
|
};
|
||||||
pub use text::{HelpFooter, NewVersionDisclaimer, Subtitle, Title};
|
pub use text::{HelpFooter, NewVersionDisclaimer, Subtitle, Title};
|
||||||
use tui_realm_stdlib::Phantom;
|
use tui_realm_stdlib::components::Phantom;
|
||||||
|
use tuirealm::component::{AppComponent, Component};
|
||||||
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
|
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
|
||||||
use tuirealm::{Component, MockComponent};
|
|
||||||
|
|
||||||
// -- global listener
|
// -- global listener
|
||||||
|
|
||||||
#[derive(Default, MockComponent)]
|
#[derive(Default, Component)]
|
||||||
pub struct GlobalListener {
|
pub struct GlobalListener {
|
||||||
component: Phantom,
|
component: Phantom,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for GlobalListener {
|
impl AppComponent<Msg, NoUserEvent> for GlobalListener {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Esc | Key::Function(10),
|
code: Key::Esc | Key::Function(10),
|
||||||
@@ -2,18 +2,22 @@
|
|||||||
//!
|
//!
|
||||||
//! auth activity bookmarks components
|
//! auth activity bookmarks components
|
||||||
|
|
||||||
use tui_realm_stdlib::{Input, List, Radio};
|
use tui_realm_stdlib::components::{Input, List, Radio};
|
||||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
use tuirealm::component::{AppComponent, Component};
|
||||||
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputType, TextSpan};
|
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
|
||||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
use tuirealm::props::{
|
||||||
|
BorderSides, BorderType, Borders, Color, HorizontalAlignment, InputType, SpanStatic, Style,
|
||||||
|
TextModifiers, Title,
|
||||||
|
};
|
||||||
|
use tuirealm::state::{State, StateValue};
|
||||||
|
|
||||||
use super::{FormMsg, Msg, UiMsg};
|
use super::{FormMsg, Msg, UiMsg};
|
||||||
use crate::ui::activities::auth::FormTab;
|
use crate::ui::activities::auth::FormTab;
|
||||||
|
|
||||||
// -- bookmark list
|
// -- bookmark list
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct BookmarksList {
|
pub struct BookmarksList {
|
||||||
component: List,
|
component: List,
|
||||||
}
|
}
|
||||||
@@ -23,23 +27,23 @@ impl BookmarksList {
|
|||||||
Self {
|
Self {
|
||||||
component: List::default()
|
component: List::default()
|
||||||
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
||||||
.highlighted_color(color)
|
.highlight_style(Style::default().fg(color))
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.scroll(true)
|
.scroll(true)
|
||||||
.step(4)
|
.step(4)
|
||||||
.title("Bookmarks", Alignment::Left)
|
.title(Title::from("Bookmarks").alignment(HorizontalAlignment::Left))
|
||||||
.rows(
|
.rows(
|
||||||
bookmarks
|
bookmarks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| vec![TextSpan::from(x.as_str())])
|
.map(|x| vec![SpanStatic::from(x.clone())])
|
||||||
.collect(),
|
.collect::<Vec<Vec<SpanStatic>>>(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for BookmarksList {
|
impl AppComponent<Msg, NoUserEvent> for BookmarksList {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Down, ..
|
code: Key::Down, ..
|
||||||
@@ -77,7 +81,7 @@ impl Component<Msg, NoUserEvent> for BookmarksList {
|
|||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Enter, ..
|
code: Key::Enter, ..
|
||||||
}) => match self.state() {
|
}) => match self.state() {
|
||||||
State::One(StateValue::Usize(choice)) => {
|
State::Single(StateValue::Usize(choice)) => {
|
||||||
Some(Msg::Form(FormMsg::LoadBookmark(choice)))
|
Some(Msg::Form(FormMsg::LoadBookmark(choice)))
|
||||||
}
|
}
|
||||||
_ => Some(Msg::None),
|
_ => Some(Msg::None),
|
||||||
@@ -98,7 +102,7 @@ impl Component<Msg, NoUserEvent> for BookmarksList {
|
|||||||
|
|
||||||
// -- recents list
|
// -- recents list
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct RecentsList {
|
pub struct RecentsList {
|
||||||
component: List,
|
component: List,
|
||||||
}
|
}
|
||||||
@@ -108,23 +112,23 @@ impl RecentsList {
|
|||||||
Self {
|
Self {
|
||||||
component: List::default()
|
component: List::default()
|
||||||
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
||||||
.highlighted_color(color)
|
.highlight_style(Style::default().fg(color))
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.scroll(true)
|
.scroll(true)
|
||||||
.step(4)
|
.step(4)
|
||||||
.title("Recent connections", Alignment::Left)
|
.title(Title::from("Recent connections").alignment(HorizontalAlignment::Left))
|
||||||
.rows(
|
.rows(
|
||||||
bookmarks
|
bookmarks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| vec![TextSpan::from(x.as_str())])
|
.map(|x| vec![SpanStatic::from(x.clone())])
|
||||||
.collect(),
|
.collect::<Vec<Vec<SpanStatic>>>(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for RecentsList {
|
impl AppComponent<Msg, NoUserEvent> for RecentsList {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Down, ..
|
code: Key::Down, ..
|
||||||
@@ -162,7 +166,7 @@ impl Component<Msg, NoUserEvent> for RecentsList {
|
|||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Enter, ..
|
code: Key::Enter, ..
|
||||||
}) => match self.state() {
|
}) => match self.state() {
|
||||||
State::One(StateValue::Usize(choice)) => {
|
State::Single(StateValue::Usize(choice)) => {
|
||||||
Some(Msg::Form(FormMsg::LoadRecent(choice)))
|
Some(Msg::Form(FormMsg::LoadRecent(choice)))
|
||||||
}
|
}
|
||||||
_ => Some(Msg::None),
|
_ => Some(Msg::None),
|
||||||
@@ -183,7 +187,7 @@ impl Component<Msg, NoUserEvent> for RecentsList {
|
|||||||
|
|
||||||
// -- delete bookmark
|
// -- delete bookmark
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct DeleteBookmarkPopup {
|
pub struct DeleteBookmarkPopup {
|
||||||
component: Radio,
|
component: Radio,
|
||||||
}
|
}
|
||||||
@@ -192,6 +196,11 @@ impl DeleteBookmarkPopup {
|
|||||||
pub fn new(color: Color) -> Self {
|
pub fn new(color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Radio::default()
|
component: Radio::default()
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(color)
|
||||||
|
.add_modifier(TextModifiers::REVERSED),
|
||||||
|
)
|
||||||
.borders(
|
.borders(
|
||||||
Borders::default()
|
Borders::default()
|
||||||
.color(color)
|
.color(color)
|
||||||
@@ -200,14 +209,15 @@ impl DeleteBookmarkPopup {
|
|||||||
.choices(["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.value(1)
|
.value(1)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.foreground(color)
|
.title(
|
||||||
.title("Delete selected bookmark?", Alignment::Center),
|
Title::from("Delete selected bookmark?").alignment(HorizontalAlignment::Center),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
impl AppComponent<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
Some(Msg::Ui(UiMsg::CloseDeleteBookmark))
|
Some(Msg::Ui(UiMsg::CloseDeleteBookmark))
|
||||||
@@ -237,7 +247,7 @@ impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
|||||||
}) => {
|
}) => {
|
||||||
if matches!(
|
if matches!(
|
||||||
self.perform(Cmd::Submit),
|
self.perform(Cmd::Submit),
|
||||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
CmdResult::Submit(State::Single(StateValue::Usize(0)))
|
||||||
) {
|
) {
|
||||||
Some(Msg::Form(FormMsg::DeleteBookmark))
|
Some(Msg::Form(FormMsg::DeleteBookmark))
|
||||||
} else {
|
} else {
|
||||||
@@ -251,7 +261,7 @@ impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
|||||||
|
|
||||||
// -- delete recent
|
// -- delete recent
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct DeleteRecentPopup {
|
pub struct DeleteRecentPopup {
|
||||||
component: Radio,
|
component: Radio,
|
||||||
}
|
}
|
||||||
@@ -260,6 +270,11 @@ impl DeleteRecentPopup {
|
|||||||
pub fn new(color: Color) -> Self {
|
pub fn new(color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Radio::default()
|
component: Radio::default()
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(color)
|
||||||
|
.add_modifier(TextModifiers::REVERSED),
|
||||||
|
)
|
||||||
.borders(
|
.borders(
|
||||||
Borders::default()
|
Borders::default()
|
||||||
.color(color)
|
.color(color)
|
||||||
@@ -268,14 +283,16 @@ impl DeleteRecentPopup {
|
|||||||
.choices(["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.value(1)
|
.value(1)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.foreground(color)
|
.title(
|
||||||
.title("Delete selected recent host?", Alignment::Center),
|
Title::from("Delete selected recent host?")
|
||||||
|
.alignment(HorizontalAlignment::Center),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
impl AppComponent<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
Some(Msg::Ui(UiMsg::CloseDeleteRecent))
|
Some(Msg::Ui(UiMsg::CloseDeleteRecent))
|
||||||
@@ -305,7 +322,7 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
|||||||
}) => {
|
}) => {
|
||||||
if matches!(
|
if matches!(
|
||||||
self.perform(Cmd::Submit),
|
self.perform(Cmd::Submit),
|
||||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
CmdResult::Submit(State::Single(StateValue::Usize(0)))
|
||||||
) {
|
) {
|
||||||
Some(Msg::Form(FormMsg::DeleteRecent))
|
Some(Msg::Form(FormMsg::DeleteRecent))
|
||||||
} else {
|
} else {
|
||||||
@@ -321,7 +338,7 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
|||||||
|
|
||||||
// -- save password
|
// -- save password
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct BookmarkSavePassword {
|
pub struct BookmarkSavePassword {
|
||||||
component: Radio,
|
component: Radio,
|
||||||
form_tab: FormTab,
|
form_tab: FormTab,
|
||||||
@@ -331,6 +348,11 @@ impl BookmarkSavePassword {
|
|||||||
pub fn new(form_tab: FormTab, color: Color) -> Self {
|
pub fn new(form_tab: FormTab, color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Radio::default()
|
component: Radio::default()
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(color)
|
||||||
|
.add_modifier(TextModifiers::REVERSED),
|
||||||
|
)
|
||||||
.borders(
|
.borders(
|
||||||
Borders::default()
|
Borders::default()
|
||||||
.color(Color::Reset)
|
.color(Color::Reset)
|
||||||
@@ -340,15 +362,14 @@ impl BookmarkSavePassword {
|
|||||||
.choices(["Yes", "No"])
|
.choices(["Yes", "No"])
|
||||||
.value(0)
|
.value(0)
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.foreground(color)
|
.title(Title::from("Save secrets?").alignment(HorizontalAlignment::Center)),
|
||||||
.title("Save secrets?", Alignment::Center),
|
|
||||||
form_tab,
|
form_tab,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
impl AppComponent<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
Some(Msg::Ui(UiMsg::CloseSaveBookmark))
|
Some(Msg::Ui(UiMsg::CloseSaveBookmark))
|
||||||
@@ -378,7 +399,7 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
|||||||
|
|
||||||
// -- new bookmark name
|
// -- new bookmark name
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct BookmarkName {
|
pub struct BookmarkName {
|
||||||
component: Input,
|
component: Input,
|
||||||
form_tab: FormTab,
|
form_tab: FormTab,
|
||||||
@@ -395,15 +416,15 @@ impl BookmarkName {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.title("Bookmark name", Alignment::Left)
|
.title(Title::from("Bookmark name").alignment(HorizontalAlignment::Left))
|
||||||
.input_type(InputType::Text),
|
.input_type(InputType::Text),
|
||||||
form_tab,
|
form_tab,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for BookmarkName {
|
impl AppComponent<Msg, NoUserEvent> for BookmarkName {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
Some(Msg::Ui(UiMsg::CloseSaveBookmark))
|
Some(Msg::Ui(UiMsg::CloseSaveBookmark))
|
||||||
@@ -447,7 +468,7 @@ impl Component<Msg, NoUserEvent> for BookmarkName {
|
|||||||
code: Key::Char(ch),
|
code: Key::Char(ch),
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
self.perform(Cmd::Type(ch));
|
self.perform(Cmd::Type(*ch));
|
||||||
Some(Msg::None)
|
Some(Msg::None)
|
||||||
}
|
}
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
|||||||
|
use tuirealm::component::{AppComponent, Component};
|
||||||
|
use tuirealm::event::NoUserEvent;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputAddress {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAddress {
|
||||||
|
pub fn new(host: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"127.0.0.1",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(Title::from("Remote host").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(host),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputAddress {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::AddressBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::AddressBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::AddressBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::AddressBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputPort {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputPort {
|
||||||
|
pub fn new(port: u16, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"22",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.input_type(InputType::UnsignedInteger)
|
||||||
|
.input_len(5)
|
||||||
|
.title(Title::from("Port number").alignment(HorizontalAlignment::Left))
|
||||||
|
.value(port.to_string()),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputPort {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PortBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PortBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PortBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PortBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputUsername {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputUsername {
|
||||||
|
pub fn new(username: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"root",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(Title::from("Username").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(username),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputUsername {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::UsernameBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::UsernameBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputPassword {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputPassword {
|
||||||
|
pub fn new(password: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.title(Title::from("Password").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Password('*'))
|
||||||
|
.value(password),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputPassword {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PasswordBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PasswordBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
use tuirealm::component::{AppComponent, Component};
|
||||||
|
use tuirealm::event::NoUserEvent;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputKubeNamespace {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputKubeNamespace {
|
||||||
|
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"namespace",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(Title::from("Pod namespace (optional)").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(bucket),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputKubeNamespace {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputKubeClusterUrl {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputKubeClusterUrl {
|
||||||
|
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"cluster url",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(
|
||||||
|
Title::from("Kube cluster url (optional)").alignment(HorizontalAlignment::Left),
|
||||||
|
)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(bucket),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputKubeClusterUrl {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurDown)),
|
||||||
|
FormTab::HostBridge => {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClusterUrlBlurDown))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClusterUrlBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputKubeUsername {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputKubeUsername {
|
||||||
|
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"username",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(Title::from("Kube username (optional)").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(bucket),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputKubeUsername {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputKubeClientCert {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputKubeClientCert {
|
||||||
|
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"/home/user/.kube/client.crt",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(
|
||||||
|
Title::from("Kube client cert path (optional)")
|
||||||
|
.alignment(HorizontalAlignment::Left),
|
||||||
|
)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(bucket),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputKubeClientCert {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurDown)),
|
||||||
|
FormTab::HostBridge => {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientCertBlurDown))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientCertBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputKubeClientKey {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputKubeClientKey {
|
||||||
|
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"/home/user/.kube/client.key",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(
|
||||||
|
Title::from("Kube client key path (optional)")
|
||||||
|
.alignment(HorizontalAlignment::Left),
|
||||||
|
)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(bucket),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputKubeClientKey {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
use tuirealm::component::{AppComponent, Component};
|
||||||
|
use tuirealm::event::NoUserEvent;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputRemoteDirectory {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputRemoteDirectory {
|
||||||
|
pub fn new(remote_dir: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"/home/foo",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(
|
||||||
|
Title::from("Default remote working directory")
|
||||||
|
.alignment(HorizontalAlignment::Left),
|
||||||
|
)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(remote_dir),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputRemoteDirectory {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurDown)),
|
||||||
|
FormTab::HostBridge => {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::RemoteDirectoryBlurDown))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::RemoteDirectoryBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputLocalDirectory {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputLocalDirectory {
|
||||||
|
pub fn new(local_dir: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"/home/foo",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(
|
||||||
|
Title::from("Default local working directory")
|
||||||
|
.alignment(HorizontalAlignment::Left),
|
||||||
|
)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(local_dir),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputLocalDirectory {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurDown)),
|
||||||
|
FormTab::HostBridge => {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::LocalDirectoryBlurDown))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::LocalDirectoryBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction};
|
||||||
|
use tuirealm::component::{AppComponent, Component};
|
||||||
|
use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
|
||||||
|
use tuirealm::props::{
|
||||||
|
BorderType, Borders, Color, HorizontalAlignment, Style, TextModifiers, Title,
|
||||||
|
};
|
||||||
|
use tuirealm::state::{State, StateValue};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct RemoteProtocolRadio {
|
||||||
|
component: Radio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemoteProtocolRadio {
|
||||||
|
pub fn new(default_protocol: FileTransferProtocol, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(color)
|
||||||
|
.add_modifier(TextModifiers::REVERSED),
|
||||||
|
)
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.choices(if cfg!(smb) {
|
||||||
|
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV", "SMB"].into_iter()
|
||||||
|
} else {
|
||||||
|
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"].into_iter()
|
||||||
|
})
|
||||||
|
.rewind(true)
|
||||||
|
.title(Title::from("Protocol").alignment(HorizontalAlignment::Left))
|
||||||
|
.value(Self::protocol_enum_to_opt(default_protocol)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol {
|
||||||
|
match protocol {
|
||||||
|
REMOTE_RADIO_PROTOCOL_SCP => FileTransferProtocol::Scp,
|
||||||
|
REMOTE_RADIO_PROTOCOL_FTP => FileTransferProtocol::Ftp(false),
|
||||||
|
REMOTE_RADIO_PROTOCOL_FTPS => FileTransferProtocol::Ftp(true),
|
||||||
|
REMOTE_RADIO_PROTOCOL_S3 => FileTransferProtocol::AwsS3,
|
||||||
|
REMOTE_RADIO_PROTOCOL_SMB => FileTransferProtocol::Smb,
|
||||||
|
REMOTE_RADIO_PROTOCOL_KUBE => FileTransferProtocol::Kube,
|
||||||
|
REMOTE_RADIO_PROTOCOL_WEBDAV => FileTransferProtocol::WebDAV,
|
||||||
|
_ => FileTransferProtocol::Sftp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize {
|
||||||
|
match protocol {
|
||||||
|
FileTransferProtocol::Sftp => REMOTE_RADIO_PROTOCOL_SFTP,
|
||||||
|
FileTransferProtocol::Scp => REMOTE_RADIO_PROTOCOL_SCP,
|
||||||
|
FileTransferProtocol::Ftp(false) => REMOTE_RADIO_PROTOCOL_FTP,
|
||||||
|
FileTransferProtocol::Ftp(true) => REMOTE_RADIO_PROTOCOL_FTPS,
|
||||||
|
FileTransferProtocol::AwsS3 => REMOTE_RADIO_PROTOCOL_S3,
|
||||||
|
FileTransferProtocol::Kube => REMOTE_RADIO_PROTOCOL_KUBE,
|
||||||
|
FileTransferProtocol::Smb => REMOTE_RADIO_PROTOCOL_SMB,
|
||||||
|
FileTransferProtocol::WebDAV => REMOTE_RADIO_PROTOCOL_WEBDAV,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for RemoteProtocolRadio {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let result = match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => self.perform(Cmd::Move(Direction::Left)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => self.perform(Cmd::Move(Direction::Right)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => return Some(Msg::Form(FormMsg::Connect)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Down, ..
|
||||||
|
}) => return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ProtocolBlurDown))),
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
|
return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ProtocolBlurUp)));
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||||
|
return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur)));
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::BackTab, ..
|
||||||
|
}) => return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ChangeFormTab))),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
CmdResult::Changed(State::Single(StateValue::Usize(choice))) => Some(Msg::Form(
|
||||||
|
FormMsg::RemoteProtocolChanged(Self::protocol_opt_to_enum(choice)),
|
||||||
|
)),
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct HostBridgeProtocolRadio {
|
||||||
|
component: Radio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostBridgeProtocolRadio {
|
||||||
|
pub fn new(protocol: HostBridgeProtocol, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(color)
|
||||||
|
.add_modifier(TextModifiers::REVERSED),
|
||||||
|
)
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.choices(if cfg!(smb) {
|
||||||
|
vec![
|
||||||
|
"Localhost",
|
||||||
|
"SFTP",
|
||||||
|
"SCP",
|
||||||
|
"FTP",
|
||||||
|
"FTPS",
|
||||||
|
"S3",
|
||||||
|
"Kube",
|
||||||
|
"WebDAV",
|
||||||
|
"SMB",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
"Localhost",
|
||||||
|
"SFTP",
|
||||||
|
"SCP",
|
||||||
|
"FTP",
|
||||||
|
"FTPS",
|
||||||
|
"S3",
|
||||||
|
"Kube",
|
||||||
|
"WebDAV",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
})
|
||||||
|
.rewind(true)
|
||||||
|
.title(Title::from("Host type").alignment(HorizontalAlignment::Left))
|
||||||
|
.value(Self::protocol_to_opt(protocol)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn protocol_to_opt(protocol: HostBridgeProtocol) -> usize {
|
||||||
|
match protocol {
|
||||||
|
HostBridgeProtocol::Localhost => HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST,
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::Sftp) => {
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_SFTP
|
||||||
|
}
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::Scp) => HOST_BRIDGE_RADIO_PROTOCOL_SCP,
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::Ftp(false)) => {
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_FTP
|
||||||
|
}
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::Ftp(true)) => {
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_FTPS
|
||||||
|
}
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::AwsS3) => {
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_S3
|
||||||
|
}
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::Smb) => HOST_BRIDGE_RADIO_PROTOCOL_SMB,
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::Kube) => {
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_KUBE
|
||||||
|
}
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::WebDAV) => {
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn protocol_opt_to_enum(protocol: usize) -> HostBridgeProtocol {
|
||||||
|
match protocol {
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST => HostBridgeProtocol::Localhost,
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_SFTP => {
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::Sftp)
|
||||||
|
}
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_SCP => HostBridgeProtocol::Remote(FileTransferProtocol::Scp),
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_FTP => {
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::Ftp(false))
|
||||||
|
}
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_FTPS => {
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::Ftp(true))
|
||||||
|
}
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_S3 => {
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::AwsS3)
|
||||||
|
}
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_SMB => HostBridgeProtocol::Remote(FileTransferProtocol::Smb),
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_KUBE => {
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::Kube)
|
||||||
|
}
|
||||||
|
HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV => {
|
||||||
|
HostBridgeProtocol::Remote(FileTransferProtocol::WebDAV)
|
||||||
|
}
|
||||||
|
_ => HostBridgeProtocol::Localhost,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for HostBridgeProtocolRadio {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let result = match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => self.perform(Cmd::Move(Direction::Left)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => self.perform(Cmd::Move(Direction::Right)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => return Some(Msg::Form(FormMsg::Connect)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Down, ..
|
||||||
|
}) => return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurDown))),
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
|
return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurUp)));
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||||
|
return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur)));
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::BackTab, ..
|
||||||
|
}) => return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ChangeFormTab))),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
CmdResult::Changed(State::Single(StateValue::Usize(choice))) => Some(Msg::Form(
|
||||||
|
FormMsg::HostBridgeProtocolChanged(Self::protocol_opt_to_enum(choice)),
|
||||||
|
)),
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
use tuirealm::command::{Cmd, Direction};
|
||||||
|
use tuirealm::component::{AppComponent, Component};
|
||||||
|
use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputS3Bucket {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputS3Bucket {
|
||||||
|
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"my-bucket",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(Title::from("Bucket name").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(bucket),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputS3Bucket {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3BucketBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3BucketBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputS3Region {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputS3Region {
|
||||||
|
pub fn new(region: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"eu-west-1",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(Title::from("Region").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(region),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputS3Region {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3RegionBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3RegionBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputS3Endpoint {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputS3Endpoint {
|
||||||
|
pub fn new(endpoint: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"http://localhost:9000",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(Title::from("Endpoint").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(endpoint),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputS3Endpoint {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct RadioS3NewPathStyle {
|
||||||
|
component: Radio,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RadioS3NewPathStyle {
|
||||||
|
pub fn new(new_path_style: bool, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(color)
|
||||||
|
.add_modifier(TextModifiers::REVERSED),
|
||||||
|
)
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.choices(["Yes", "No"])
|
||||||
|
.rewind(true)
|
||||||
|
.title(Title::from("New path style").alignment(HorizontalAlignment::Left))
|
||||||
|
.value(usize::from(!new_path_style)),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for RadioS3NewPathStyle {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => Some(Msg::Form(FormMsg::Connect)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Down, ..
|
||||||
|
}) => Some(if self.form_tab == FormTab::Remote {
|
||||||
|
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3NewPathStyleBlurDown))
|
||||||
|
} else {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3NewPathStyleBlurDown))
|
||||||
|
}),
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
|
Some(if self.form_tab == FormTab::Remote {
|
||||||
|
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3NewPathStyleBlurUp))
|
||||||
|
} else {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3NewPathStyleBlurUp))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||||
|
Some(if self.form_tab == FormTab::Remote {
|
||||||
|
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur))
|
||||||
|
} else {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputS3Profile {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputS3Profile {
|
||||||
|
pub fn new(profile: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"default",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(Title::from("Profile").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(profile),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputS3Profile {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputS3AccessKey {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputS3AccessKey {
|
||||||
|
pub fn new(access_key: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"AKIA...",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(Title::from("Access key").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(access_key),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputS3AccessKey {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputS3SecretAccessKey {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputS3SecretAccessKey {
|
||||||
|
pub fn new(secret_access_key: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.title(Title::from("Secret access key").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Password('*'))
|
||||||
|
.value(secret_access_key),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputS3SecretAccessKey {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurDown)),
|
||||||
|
FormTab::HostBridge => {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SecretAccessKeyBlurDown))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurUp)),
|
||||||
|
FormTab::HostBridge => {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SecretAccessKeyBlurUp))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputS3SecurityToken {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputS3SecurityToken {
|
||||||
|
pub fn new(security_token: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.title(Title::from("Security token").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Password('*'))
|
||||||
|
.value(security_token),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputS3SecurityToken {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurDown)),
|
||||||
|
FormTab::HostBridge => {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SecurityTokenBlurDown))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SecurityTokenBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputS3SessionToken {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputS3SessionToken {
|
||||||
|
pub fn new(session_token: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.title(Title::from("Session token").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Password('*'))
|
||||||
|
.value(session_token),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputS3SessionToken {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SessionTokenBlurDown)),
|
||||||
|
FormTab::HostBridge => {
|
||||||
|
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SessionTokenBlurDown))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SessionTokenBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SessionTokenBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
use tuirealm::component::{AppComponent, Component};
|
||||||
|
use tuirealm::event::NoUserEvent;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputSmbShare {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputSmbShare {
|
||||||
|
pub fn new(host: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.title(Title::from("Share").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(host),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputSmbShare {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbShareBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbShareBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbShareBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbShareBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputSmbWorkgroup {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
impl InputSmbWorkgroup {
|
||||||
|
pub fn new(host: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.title(Title::from("Workgroup").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(host),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputSmbWorkgroup {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
use tuirealm::component::{AppComponent, Component};
|
||||||
|
use tuirealm::event::NoUserEvent;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InputWebDAVUri {
|
||||||
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputWebDAVUri {
|
||||||
|
pub fn new(host: &str, form_tab: FormTab, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||||
|
"http://localhost:8080",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
))
|
||||||
|
.title(Title::from("HTTP url").alignment(HorizontalAlignment::Left))
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.value(host),
|
||||||
|
form_tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppComponent<Msg, NoUserEvent> for InputWebDAVUri {
|
||||||
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
let on_key_down = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::WebDAVUriBlurDown)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurDown)),
|
||||||
|
};
|
||||||
|
let on_key_up = match self.form_tab {
|
||||||
|
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::WebDAVUriBlurUp)),
|
||||||
|
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurUp)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let form_tab = self.form_tab;
|
||||||
|
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,17 +2,23 @@
|
|||||||
//!
|
//!
|
||||||
//! auth activity popups
|
//! auth activity popups
|
||||||
|
|
||||||
use tui_realm_stdlib::{List, Paragraph, Radio, Textarea};
|
use tui_realm_stdlib::components::{List, Paragraph, Radio, Textarea};
|
||||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
use tuirealm::component::{AppComponent, Component};
|
||||||
use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan};
|
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
|
||||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
use tuirealm::props::{
|
||||||
|
BorderType, Borders, Color, HorizontalAlignment, SpanStatic, Style, TableBuilder,
|
||||||
|
TextModifiers, Title,
|
||||||
|
};
|
||||||
|
use tuirealm::ratatui::style::Stylize;
|
||||||
|
use tuirealm::ratatui::text::Text;
|
||||||
|
use tuirealm::state::{State, StateValue};
|
||||||
|
|
||||||
use super::{FormMsg, Msg, UiMsg};
|
use super::{FormMsg, Msg, UiMsg};
|
||||||
|
|
||||||
// -- error popup
|
// -- error popup
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct ErrorPopup {
|
pub struct ErrorPopup {
|
||||||
component: Paragraph,
|
component: Paragraph,
|
||||||
}
|
}
|
||||||
@@ -21,21 +27,23 @@ impl ErrorPopup {
|
|||||||
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Paragraph::default()
|
component: Paragraph::default()
|
||||||
.alignment(Alignment::Center)
|
.alignment_horizontal(HorizontalAlignment::Center)
|
||||||
.borders(
|
.borders(
|
||||||
Borders::default()
|
Borders::default()
|
||||||
.color(color)
|
.color(color)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text([TextSpan::from(text.as_ref())])
|
.text(Text::from_iter([SpanStatic::from(
|
||||||
.wrap(true),
|
text.as_ref().to_string(),
|
||||||
|
)]))
|
||||||
|
.wrap_trim(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for ErrorPopup {
|
impl AppComponent<Msg, NoUserEvent> for ErrorPopup {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Esc | Key::Enter,
|
code: Key::Esc | Key::Enter,
|
||||||
@@ -48,7 +56,7 @@ impl Component<Msg, NoUserEvent> for ErrorPopup {
|
|||||||
|
|
||||||
// -- info popup
|
// -- info popup
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct InfoPopup {
|
pub struct InfoPopup {
|
||||||
component: Paragraph,
|
component: Paragraph,
|
||||||
}
|
}
|
||||||
@@ -57,21 +65,23 @@ impl InfoPopup {
|
|||||||
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Paragraph::default()
|
component: Paragraph::default()
|
||||||
.alignment(Alignment::Center)
|
.alignment_horizontal(HorizontalAlignment::Center)
|
||||||
.borders(
|
.borders(
|
||||||
Borders::default()
|
Borders::default()
|
||||||
.color(color)
|
.color(color)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text([TextSpan::from(text.as_ref())])
|
.text(Text::from_iter([SpanStatic::from(
|
||||||
.wrap(true),
|
text.as_ref().to_string(),
|
||||||
|
)]))
|
||||||
|
.wrap_trim(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for InfoPopup {
|
impl AppComponent<Msg, NoUserEvent> for InfoPopup {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Esc | Key::Enter,
|
code: Key::Esc | Key::Enter,
|
||||||
@@ -84,7 +94,7 @@ impl Component<Msg, NoUserEvent> for InfoPopup {
|
|||||||
|
|
||||||
// -- wait popup
|
// -- wait popup
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct WaitPopup {
|
pub struct WaitPopup {
|
||||||
component: Paragraph,
|
component: Paragraph,
|
||||||
}
|
}
|
||||||
@@ -93,28 +103,30 @@ impl WaitPopup {
|
|||||||
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Paragraph::default()
|
component: Paragraph::default()
|
||||||
.alignment(Alignment::Center)
|
.alignment_horizontal(HorizontalAlignment::Center)
|
||||||
.borders(
|
.borders(
|
||||||
Borders::default()
|
Borders::default()
|
||||||
.color(color)
|
.color(color)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text([TextSpan::from(text.as_ref())])
|
.text(Text::from_iter([SpanStatic::from(
|
||||||
.wrap(true),
|
text.as_ref().to_string(),
|
||||||
|
)]))
|
||||||
|
.wrap_trim(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for WaitPopup {
|
impl AppComponent<Msg, NoUserEvent> for WaitPopup {
|
||||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- window size error
|
// -- window size error
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct WindowSizeError {
|
pub struct WindowSizeError {
|
||||||
component: Paragraph,
|
component: Paragraph,
|
||||||
}
|
}
|
||||||
@@ -123,30 +135,30 @@ impl WindowSizeError {
|
|||||||
pub fn new(color: Color) -> Self {
|
pub fn new(color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Paragraph::default()
|
component: Paragraph::default()
|
||||||
.alignment(Alignment::Center)
|
.alignment_horizontal(HorizontalAlignment::Center)
|
||||||
.borders(
|
.borders(
|
||||||
Borders::default()
|
Borders::default()
|
||||||
.color(color)
|
.color(color)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.text([TextSpan::from(
|
.text(Text::from_iter([SpanStatic::from(
|
||||||
"termscp requires at least 24 lines of height to run",
|
"termscp requires at least 24 lines of height to run",
|
||||||
)])
|
)]))
|
||||||
.wrap(true),
|
.wrap_trim(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for WindowSizeError {
|
impl AppComponent<Msg, NoUserEvent> for WindowSizeError {
|
||||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- quit popup
|
// -- quit popup
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct QuitPopup {
|
pub struct QuitPopup {
|
||||||
component: Radio,
|
component: Radio,
|
||||||
}
|
}
|
||||||
@@ -155,21 +167,25 @@ impl QuitPopup {
|
|||||||
pub fn new(color: Color) -> Self {
|
pub fn new(color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Radio::default()
|
component: Radio::default()
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(color)
|
||||||
|
.add_modifier(TextModifiers::REVERSED),
|
||||||
|
)
|
||||||
.borders(
|
.borders(
|
||||||
Borders::default()
|
Borders::default()
|
||||||
.color(color)
|
.color(color)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.title(Title::from("Quit termscp?").alignment(HorizontalAlignment::Center))
|
||||||
.title("Quit termscp?", Alignment::Center)
|
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.choices(["Yes", "No"]),
|
.choices(["Yes", "No"]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for QuitPopup {
|
impl AppComponent<Msg, NoUserEvent> for QuitPopup {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
Some(Msg::Ui(UiMsg::CloseQuitPopup))
|
Some(Msg::Ui(UiMsg::CloseQuitPopup))
|
||||||
@@ -199,7 +215,7 @@ impl Component<Msg, NoUserEvent> for QuitPopup {
|
|||||||
}) => {
|
}) => {
|
||||||
if matches!(
|
if matches!(
|
||||||
self.perform(Cmd::Submit),
|
self.perform(Cmd::Submit),
|
||||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
CmdResult::Submit(State::Single(StateValue::Usize(0)))
|
||||||
) {
|
) {
|
||||||
Some(Msg::Form(FormMsg::Quit))
|
Some(Msg::Form(FormMsg::Quit))
|
||||||
} else {
|
} else {
|
||||||
@@ -213,7 +229,7 @@ impl Component<Msg, NoUserEvent> for QuitPopup {
|
|||||||
|
|
||||||
// -- install update popup
|
// -- install update popup
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct InstallUpdatePopup {
|
pub struct InstallUpdatePopup {
|
||||||
component: Radio,
|
component: Radio,
|
||||||
}
|
}
|
||||||
@@ -222,21 +238,25 @@ impl InstallUpdatePopup {
|
|||||||
pub fn new(color: Color) -> Self {
|
pub fn new(color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Radio::default()
|
component: Radio::default()
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(color)
|
||||||
|
.add_modifier(TextModifiers::REVERSED),
|
||||||
|
)
|
||||||
.borders(
|
.borders(
|
||||||
Borders::default()
|
Borders::default()
|
||||||
.color(color)
|
.color(color)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.title(Title::from("Install update?").alignment(HorizontalAlignment::Center))
|
||||||
.title("Install update?", Alignment::Center)
|
|
||||||
.rewind(true)
|
.rewind(true)
|
||||||
.choices(["Yes", "No"]),
|
.choices(["Yes", "No"]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
|
impl AppComponent<Msg, NoUserEvent> for InstallUpdatePopup {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
Some(Msg::Ui(UiMsg::CloseInstallUpdatePopup))
|
Some(Msg::Ui(UiMsg::CloseInstallUpdatePopup))
|
||||||
@@ -266,7 +286,7 @@ impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
|
|||||||
}) => {
|
}) => {
|
||||||
if matches!(
|
if matches!(
|
||||||
self.perform(Cmd::Submit),
|
self.perform(Cmd::Submit),
|
||||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
CmdResult::Submit(State::Single(StateValue::Usize(0)))
|
||||||
) {
|
) {
|
||||||
Some(Msg::Form(FormMsg::InstallUpdate))
|
Some(Msg::Form(FormMsg::InstallUpdate))
|
||||||
} else {
|
} else {
|
||||||
@@ -280,7 +300,7 @@ impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
|
|||||||
|
|
||||||
// -- release notes popup
|
// -- release notes popup
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct ReleaseNotes {
|
pub struct ReleaseNotes {
|
||||||
component: Textarea,
|
component: Textarea,
|
||||||
}
|
}
|
||||||
@@ -295,14 +315,14 @@ impl ReleaseNotes {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.title("Release notes", Alignment::Center)
|
.title(Title::from("Release notes").alignment(HorizontalAlignment::Center))
|
||||||
.text_rows(notes.lines().map(TextSpan::from)),
|
.text_rows(notes.lines().map(|l| SpanStatic::from(l.to_string()))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for ReleaseNotes {
|
impl AppComponent<Msg, NoUserEvent> for ReleaseNotes {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Esc | Key::Enter,
|
code: Key::Esc | Key::Enter,
|
||||||
@@ -348,7 +368,7 @@ impl Component<Msg, NoUserEvent> for ReleaseNotes {
|
|||||||
|
|
||||||
// -- keybindings popup
|
// -- keybindings popup
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct Keybindings {
|
pub struct Keybindings {
|
||||||
component: List,
|
component: List,
|
||||||
}
|
}
|
||||||
@@ -362,43 +382,47 @@ impl Keybindings {
|
|||||||
.color(color)
|
.color(color)
|
||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.highlighted_str("? ")
|
.highlight_str("? ")
|
||||||
.title("Keybindings", Alignment::Center)
|
.title(Title::from("Keybindings").alignment(HorizontalAlignment::Center))
|
||||||
.scroll(true)
|
.scroll(true)
|
||||||
.step(4)
|
.step(4)
|
||||||
.rows(
|
.rows(
|
||||||
TableBuilder::default()
|
TableBuilder::default()
|
||||||
.add_col(TextSpan::new("<ESC>").bold().fg(color))
|
.add_col(SpanStatic::raw("<ESC>").bold().fg(color))
|
||||||
.add_col(TextSpan::from(" Quit termscp"))
|
.add_col(SpanStatic::from(" Quit termscp"))
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::new("<TAB>").bold().fg(color))
|
.add_col(SpanStatic::raw("<TAB>").bold().fg(color))
|
||||||
.add_col(TextSpan::from(" Switch from form and bookmarks"))
|
.add_col(SpanStatic::from(
|
||||||
|
" Switch from form and bookmarks",
|
||||||
|
))
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(color))
|
.add_col(SpanStatic::raw("<RIGHT/LEFT>").bold().fg(color))
|
||||||
.add_col(TextSpan::from(" Switch bookmark tab"))
|
.add_col(SpanStatic::from(" Switch bookmark tab"))
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(color))
|
.add_col(SpanStatic::raw("<UP/DOWN>").bold().fg(color))
|
||||||
.add_col(TextSpan::from(" Move up/down in current tab"))
|
.add_col(SpanStatic::from(" Move up/down in current tab"))
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::new("<ENTER>").bold().fg(color))
|
.add_col(SpanStatic::raw("<ENTER>").bold().fg(color))
|
||||||
.add_col(TextSpan::from(" Connect/Load bookmark"))
|
.add_col(SpanStatic::from(" Connect/Load bookmark"))
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::new("<DEL|E>").bold().fg(color))
|
.add_col(SpanStatic::raw("<DEL|E>").bold().fg(color))
|
||||||
.add_col(TextSpan::from(" Delete selected bookmark"))
|
.add_col(SpanStatic::from(" Delete selected bookmark"))
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::new("<CTRL+C>").bold().fg(color))
|
.add_col(SpanStatic::raw("<CTRL+C>").bold().fg(color))
|
||||||
.add_col(TextSpan::from(" Enter setup"))
|
.add_col(SpanStatic::from(" Enter setup"))
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::new("<CTRL+S>").bold().fg(color))
|
.add_col(SpanStatic::raw("<CTRL+S>").bold().fg(color))
|
||||||
.add_col(TextSpan::from(" Save bookmark"))
|
.add_col(SpanStatic::from(" Save bookmark"))
|
||||||
.build(),
|
.build()
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| row.into_iter().flat_map(|l| l.spans).collect::<Vec<_>>()),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for Keybindings {
|
impl AppComponent<Msg, NoUserEvent> for Keybindings {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Esc | Key::Enter,
|
code: Key::Esc | Key::Enter,
|
||||||
|
|||||||
@@ -2,15 +2,17 @@
|
|||||||
//!
|
//!
|
||||||
//! auth activity texts
|
//! auth activity texts
|
||||||
|
|
||||||
use tui_realm_stdlib::{Label, Span};
|
use tui_realm_stdlib::components::{Label, Span};
|
||||||
use tuirealm::props::{Color, TextModifiers, TextSpan};
|
use tuirealm::component::{AppComponent, Component};
|
||||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
use tuirealm::event::{Event, NoUserEvent};
|
||||||
|
use tuirealm::props::{Color, SpanStatic, TextModifiers};
|
||||||
|
use tuirealm::ratatui::style::Stylize;
|
||||||
|
|
||||||
use super::Msg;
|
use super::Msg;
|
||||||
|
|
||||||
// -- Title
|
// -- Title
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct Title {
|
pub struct Title {
|
||||||
component: Label,
|
component: Label,
|
||||||
}
|
}
|
||||||
@@ -25,15 +27,15 @@ impl Default for Title {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for Title {
|
impl AppComponent<Msg, NoUserEvent> for Title {
|
||||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- subtitle
|
// -- subtitle
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct Subtitle {
|
pub struct Subtitle {
|
||||||
component: Label,
|
component: Label,
|
||||||
}
|
}
|
||||||
@@ -48,15 +50,15 @@ impl Default for Subtitle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for Subtitle {
|
impl AppComponent<Msg, NoUserEvent> for Subtitle {
|
||||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- new version disclaimer
|
// -- new version disclaimer
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct NewVersionDisclaimer {
|
pub struct NewVersionDisclaimer {
|
||||||
component: Span,
|
component: Span,
|
||||||
}
|
}
|
||||||
@@ -65,9 +67,9 @@ impl NewVersionDisclaimer {
|
|||||||
pub fn new(new_version: &str, color: Color) -> Self {
|
pub fn new(new_version: &str, color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Span::default().foreground(color).spans([
|
component: Span::default().foreground(color).spans([
|
||||||
TextSpan::from("termscp "),
|
SpanStatic::from("termscp "),
|
||||||
TextSpan::new(new_version).underlined().bold(),
|
SpanStatic::raw(new_version.to_string()).underlined().bold(),
|
||||||
TextSpan::from(
|
SpanStatic::from(
|
||||||
" is NOW available! Install update and view release notes with <CTRL+R>",
|
" is NOW available! Install update and view release notes with <CTRL+R>",
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
@@ -75,15 +77,15 @@ impl NewVersionDisclaimer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for NewVersionDisclaimer {
|
impl AppComponent<Msg, NoUserEvent> for NewVersionDisclaimer {
|
||||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- HelpFooter
|
// -- HelpFooter
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Component)]
|
||||||
pub struct HelpFooter {
|
pub struct HelpFooter {
|
||||||
component: Span,
|
component: Span,
|
||||||
}
|
}
|
||||||
@@ -92,27 +94,27 @@ impl HelpFooter {
|
|||||||
pub fn new(key_color: Color) -> Self {
|
pub fn new(key_color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Span::default().spans([
|
component: Span::default().spans([
|
||||||
TextSpan::from("<F1|CTRL+H>").bold().fg(key_color),
|
SpanStatic::from("<F1|CTRL+H>").bold().fg(key_color),
|
||||||
TextSpan::from(" Help "),
|
SpanStatic::from(" Help "),
|
||||||
TextSpan::from("<CTRL+C>").bold().fg(key_color),
|
SpanStatic::from("<CTRL+C>").bold().fg(key_color),
|
||||||
TextSpan::from(" Enter setup "),
|
SpanStatic::from(" Enter setup "),
|
||||||
TextSpan::from("<UP/DOWN>").bold().fg(key_color),
|
SpanStatic::from("<UP/DOWN>").bold().fg(key_color),
|
||||||
TextSpan::from(" Change field "),
|
SpanStatic::from(" Change field "),
|
||||||
TextSpan::from("<TAB>").bold().fg(key_color),
|
SpanStatic::from("<TAB>").bold().fg(key_color),
|
||||||
TextSpan::from(" Switch tab "),
|
SpanStatic::from(" Switch tab "),
|
||||||
TextSpan::from("<BACKTAB>").bold().fg(key_color),
|
SpanStatic::from("<BACKTAB>").bold().fg(key_color),
|
||||||
TextSpan::from(" Switch form "),
|
SpanStatic::from(" Switch form "),
|
||||||
TextSpan::from("<ENTER>").bold().fg(key_color),
|
SpanStatic::from("<ENTER>").bold().fg(key_color),
|
||||||
TextSpan::from(" Submit form "),
|
SpanStatic::from(" Submit form "),
|
||||||
TextSpan::from("<F10|ESC>").bold().fg(key_color),
|
SpanStatic::from("<F10|ESC>").bold().fg(key_color),
|
||||||
TextSpan::from(" Quit "),
|
SpanStatic::from(" Quit "),
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for HelpFooter {
|
impl AppComponent<Msg, NoUserEvent> for HelpFooter {
|
||||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+476
-744
File diff suppressed because it is too large
Load Diff
+131
-1873
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,679 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::ui::activities::auth::STORE_KEY_RELEASE_NOTES;
|
||||||
|
|
||||||
|
impl AuthActivity {
|
||||||
|
/// Make text span from bookmarks
|
||||||
|
pub(in crate::ui::activities::auth) fn view_bookmarks(&mut self) {
|
||||||
|
let bookmarks: Vec<String> = self
|
||||||
|
.bookmarks_list
|
||||||
|
.iter()
|
||||||
|
.map(|x| {
|
||||||
|
Self::fmt_bookmark(x, self.bookmarks_client().unwrap().get_bookmark(x).unwrap())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let bookmarks_color = self.theme().auth_bookmarks;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::BookmarksList,
|
||||||
|
Box::new(components::BookmarksList::new(&bookmarks, bookmarks_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// View recent connections
|
||||||
|
pub(in crate::ui::activities::auth) fn view_recent_connections(&mut self) {
|
||||||
|
let bookmarks: Vec<String> = self
|
||||||
|
.recents_list
|
||||||
|
.iter()
|
||||||
|
.map(|x| Self::fmt_recent(self.bookmarks_client().unwrap().get_recent(x).unwrap()))
|
||||||
|
.collect();
|
||||||
|
let recents_color = self.theme().auth_recents;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::RecentsList,
|
||||||
|
Box::new(components::RecentsList::new(&bookmarks, recents_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_error<S: AsRef<str>>(&mut self, text: S) {
|
||||||
|
let err_color = self.theme().misc_error_dialog;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::ErrorPopup,
|
||||||
|
Box::new(components::ErrorPopup::new(text, err_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.active(&Id::ErrorPopup) {
|
||||||
|
error!("Failed to activate component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn umount_error(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::ErrorPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_info<S: AsRef<str>>(&mut self, text: S) {
|
||||||
|
let color = self.theme().misc_info_dialog;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::InfoPopup,
|
||||||
|
Box::new(components::InfoPopup::new(text, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.active(&Id::InfoPopup) {
|
||||||
|
error!("Failed to activate component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn umount_info(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::InfoPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_wait(&mut self, text: &str) {
|
||||||
|
let wait_color = self.theme().misc_info_dialog;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::WaitPopup,
|
||||||
|
Box::new(components::WaitPopup::new(text, wait_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.active(&Id::WaitPopup) {
|
||||||
|
error!("Failed to activate component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn umount_wait(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::WaitPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_size_err(&mut self) {
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::WindowSizeError,
|
||||||
|
Box::new(components::WindowSizeError::new(Color::Red)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.active(&Id::WindowSizeError) {
|
||||||
|
error!("Failed to activate component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn umount_size_err(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::WindowSizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_quit(&mut self) {
|
||||||
|
let quit_color = self.theme().misc_quit_dialog;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::QuitPopup,
|
||||||
|
Box::new(components::QuitPopup::new(quit_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.active(&Id::QuitPopup) {
|
||||||
|
error!("Failed to activate component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn umount_quit(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::QuitPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_bookmark_del_dialog(&mut self) {
|
||||||
|
let warn_color = self.theme().misc_warn_dialog;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::DeleteBookmarkPopup,
|
||||||
|
Box::new(components::DeleteBookmarkPopup::new(warn_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.active(&Id::DeleteBookmarkPopup) {
|
||||||
|
error!("Failed to activate component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn umount_bookmark_del_dialog(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::DeleteBookmarkPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_recent_del_dialog(&mut self) {
|
||||||
|
let warn_color = self.theme().misc_warn_dialog;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::DeleteRecentPopup,
|
||||||
|
Box::new(components::DeleteRecentPopup::new(warn_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.active(&Id::DeleteRecentPopup) {
|
||||||
|
error!("Failed to activate component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn umount_recent_del_dialog(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::DeleteRecentPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_bookmark_save_dialog(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) {
|
||||||
|
let save_color = self.theme().misc_save_dialog;
|
||||||
|
let warn_color = self.theme().misc_warn_dialog;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::BookmarkName,
|
||||||
|
Box::new(components::BookmarkName::new(form_tab, save_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::BookmarkSavePassword,
|
||||||
|
Box::new(components::BookmarkSavePassword::new(form_tab, warn_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.active(&Id::BookmarkName) {
|
||||||
|
error!("Failed to activate component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn umount_bookmark_save_dialog(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::BookmarkName);
|
||||||
|
let _ = self.app.umount(&Id::BookmarkSavePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_keybindings(&mut self) {
|
||||||
|
let key_color = self.theme().misc_keys;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::Keybindings,
|
||||||
|
Box::new(components::Keybindings::new(key_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.active(&Id::Keybindings) {
|
||||||
|
error!("Failed to activate component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn umount_help(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::Keybindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_release_notes(&mut self) {
|
||||||
|
if let Some(ctx) = self.context.as_ref()
|
||||||
|
&& let Some(release_notes) = ctx.store().get_string(STORE_KEY_RELEASE_NOTES)
|
||||||
|
{
|
||||||
|
let info_color = self.theme().misc_info_dialog;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::NewVersionChangelog,
|
||||||
|
Box::new(components::ReleaseNotes::new(release_notes, info_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::InstallUpdatePopup,
|
||||||
|
Box::new(components::InstallUpdatePopup::new(info_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
if let Err(err) = self.app.active(&Id::InstallUpdatePopup) {
|
||||||
|
error!("Failed to activate component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn umount_release_notes(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::NewVersionChangelog);
|
||||||
|
let _ = self.app.umount(&Id::InstallUpdatePopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_host_bridge_protocol(
|
||||||
|
&mut self,
|
||||||
|
protocol: HostBridgeProtocol,
|
||||||
|
) {
|
||||||
|
let protocol_color = self.theme().auth_protocol;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::HostBridge(AuthFormId::Protocol),
|
||||||
|
Box::new(components::HostBridgeProtocolRadio::new(
|
||||||
|
protocol,
|
||||||
|
protocol_color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_remote_protocol(
|
||||||
|
&mut self,
|
||||||
|
protocol: FileTransferProtocol,
|
||||||
|
) {
|
||||||
|
let protocol_color = self.theme().auth_protocol;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
Id::Remote(AuthFormId::Protocol),
|
||||||
|
Box::new(components::RemoteProtocolRadio::new(
|
||||||
|
protocol,
|
||||||
|
protocol_color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_remote_directory<S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
remote_path: S,
|
||||||
|
) {
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::RemoteDirectory);
|
||||||
|
let protocol_color = self.theme().auth_protocol;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputRemoteDirectory::new(
|
||||||
|
remote_path.as_ref(),
|
||||||
|
form_tab,
|
||||||
|
protocol_color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_local_directory<S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
local_path: S,
|
||||||
|
) {
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::LocalDirectory);
|
||||||
|
let color = self.theme().auth_username;
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputLocalDirectory::new(
|
||||||
|
local_path.as_ref(),
|
||||||
|
form_tab,
|
||||||
|
color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_address(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
address: &str,
|
||||||
|
) {
|
||||||
|
let addr_color = self.theme().auth_address;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::Address);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputAddress::new(address, form_tab, addr_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_port(&mut self, form_tab: FormTab, port: u16) {
|
||||||
|
let port_color = self.theme().auth_port;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::Port);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputPort::new(port, form_tab, port_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_username(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
username: &str,
|
||||||
|
) {
|
||||||
|
let username_color = self.theme().auth_username;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::Username);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputUsername::new(
|
||||||
|
username,
|
||||||
|
form_tab,
|
||||||
|
username_color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_password(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
password: &str,
|
||||||
|
) {
|
||||||
|
let password_color = self.theme().auth_password;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::Password);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputPassword::new(
|
||||||
|
password,
|
||||||
|
form_tab,
|
||||||
|
password_color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_s3_bucket(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
bucket: &str,
|
||||||
|
) {
|
||||||
|
let addr_color = self.theme().auth_address;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::S3Bucket);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputS3Bucket::new(bucket, form_tab, addr_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_s3_region(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
region: &str,
|
||||||
|
) {
|
||||||
|
let port_color = self.theme().auth_port;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::S3Region);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputS3Region::new(region, form_tab, port_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_s3_endpoint(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
endpoint: &str,
|
||||||
|
) {
|
||||||
|
let username_color = self.theme().auth_username;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::S3Endpoint);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputS3Endpoint::new(
|
||||||
|
endpoint,
|
||||||
|
form_tab,
|
||||||
|
username_color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_s3_profile(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
profile: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_password;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::S3Profile);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputS3Profile::new(profile, form_tab, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_s3_access_key(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
key: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_address;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::S3AccessKey);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputS3AccessKey::new(key, form_tab, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_s3_secret_access_key(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
key: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_port;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::S3SecretAccessKey);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputS3SecretAccessKey::new(
|
||||||
|
key, form_tab, color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_s3_security_token(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
token: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_username;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::S3SecurityToken);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputS3SecurityToken::new(
|
||||||
|
token, form_tab, color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_s3_session_token(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
token: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_password;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::S3SessionToken);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputS3SessionToken::new(token, form_tab, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_s3_new_path_style(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
new_path_style: bool,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_address;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::S3NewPathStyle);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::RadioS3NewPathStyle::new(
|
||||||
|
new_path_style,
|
||||||
|
form_tab,
|
||||||
|
color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_kube_namespace(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
value: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_port;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::KubeNamespace);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputKubeNamespace::new(value, form_tab, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_kube_cluster_url(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
value: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_username;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::KubeClusterUrl);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputKubeClusterUrl::new(value, form_tab, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_kube_username(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
value: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_password;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::KubeUsername);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputKubeUsername::new(value, form_tab, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_kube_client_cert(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
value: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_address;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::KubeClientCert);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputKubeClientCert::new(value, form_tab, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_kube_client_key(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
value: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_port;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::KubeClientKey);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputKubeClientKey::new(value, form_tab, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_smb_share(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
share: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_password;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::SmbShare);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputSmbShare::new(share, form_tab, color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_smb_workgroup(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
workgroup: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().auth_address;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::SmbWorkgroup);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputSmbWorkgroup::new(
|
||||||
|
workgroup, form_tab, color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn mount_webdav_uri(
|
||||||
|
&mut self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
uri: &str,
|
||||||
|
) {
|
||||||
|
let addr_color = self.theme().auth_address;
|
||||||
|
let id = Self::form_tab_id(form_tab, AuthFormId::WebDAVUri);
|
||||||
|
if let Err(err) = self.app.remount(
|
||||||
|
id,
|
||||||
|
Box::new(components::InputWebDAVUri::new(uri, form_tab, addr_color)),
|
||||||
|
vec![],
|
||||||
|
) {
|
||||||
|
error!("Failed to remount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn form_tab_id(form_tab: FormTab, id: AuthFormId) -> Id {
|
||||||
|
match form_tab {
|
||||||
|
FormTab::HostBridge => Id::HostBridge(id),
|
||||||
|
FormTab::Remote => Id::Remote(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,512 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use tuirealm::state::{State, StateValue};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::filetransfer::FileTransferParams;
|
||||||
|
use crate::filetransfer::params::{
|
||||||
|
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
|
||||||
|
WebDAVProtocolParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl AuthActivity {
|
||||||
|
pub(in crate::ui::activities::auth) fn get_generic_params_input(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> GenericProtocolParams {
|
||||||
|
let addr = self.get_input_addr(form_tab);
|
||||||
|
let port = self.get_input_port(form_tab);
|
||||||
|
let username = self.get_input_username(form_tab);
|
||||||
|
let password = self.get_input_password(form_tab);
|
||||||
|
GenericProtocolParams::default()
|
||||||
|
.address(addr)
|
||||||
|
.port(port)
|
||||||
|
.username(username)
|
||||||
|
.password(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_s3_params_input(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> AwsS3Params {
|
||||||
|
let bucket = self.get_input_s3_bucket(form_tab);
|
||||||
|
let region = self.get_input_s3_region(form_tab);
|
||||||
|
let endpoint = self.get_input_s3_endpoint(form_tab);
|
||||||
|
let profile = self.get_input_s3_profile(form_tab);
|
||||||
|
let access_key = self.get_input_s3_access_key(form_tab);
|
||||||
|
let secret_access_key = self.get_input_s3_secret_access_key(form_tab);
|
||||||
|
let security_token = self.get_input_s3_security_token(form_tab);
|
||||||
|
let session_token = self.get_input_s3_session_token(form_tab);
|
||||||
|
let new_path_style = self.get_input_s3_new_path_style(form_tab);
|
||||||
|
AwsS3Params::new(bucket, region, profile)
|
||||||
|
.endpoint(endpoint)
|
||||||
|
.access_key(access_key)
|
||||||
|
.secret_access_key(secret_access_key)
|
||||||
|
.security_token(security_token)
|
||||||
|
.session_token(session_token)
|
||||||
|
.new_path_style(new_path_style)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_kube_params_input(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> KubeProtocolParams {
|
||||||
|
let namespace = self.get_input_kube_namespace(form_tab);
|
||||||
|
let cluster_url = self.get_input_kube_cluster_url(form_tab);
|
||||||
|
let username = self.get_input_kube_username(form_tab);
|
||||||
|
let client_cert = self.get_input_kube_client_cert(form_tab);
|
||||||
|
let client_key = self.get_input_kube_client_key(form_tab);
|
||||||
|
KubeProtocolParams {
|
||||||
|
namespace,
|
||||||
|
cluster_url,
|
||||||
|
username,
|
||||||
|
client_cert,
|
||||||
|
client_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
pub(in crate::ui::activities::auth) fn get_smb_params_input(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> SmbParams {
|
||||||
|
let share = self.get_input_smb_share(form_tab);
|
||||||
|
let workgroup = self.get_input_smb_workgroup(form_tab);
|
||||||
|
let address = self.get_input_addr(form_tab);
|
||||||
|
let port = self.get_input_port(form_tab);
|
||||||
|
let username = self.get_input_username(form_tab);
|
||||||
|
let password = self.get_input_password(form_tab);
|
||||||
|
|
||||||
|
SmbParams::new(address, share)
|
||||||
|
.port(port)
|
||||||
|
.username(username)
|
||||||
|
.password(password)
|
||||||
|
.workgroup(workgroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(win)]
|
||||||
|
pub(in crate::ui::activities::auth) fn get_smb_params_input(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> SmbParams {
|
||||||
|
let share = self.get_input_smb_share(form_tab);
|
||||||
|
let address = self.get_input_addr(form_tab);
|
||||||
|
let username = self.get_input_username(form_tab);
|
||||||
|
let password = self.get_input_password(form_tab);
|
||||||
|
|
||||||
|
SmbParams::new(address, share)
|
||||||
|
.username(username)
|
||||||
|
.password(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_webdav_params_input(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> WebDAVProtocolParams {
|
||||||
|
let uri = self.get_webdav_uri(form_tab);
|
||||||
|
let username = self.get_input_username(form_tab).unwrap_or_default();
|
||||||
|
let password = self.get_input_password(form_tab).unwrap_or_default();
|
||||||
|
|
||||||
|
WebDAVProtocolParams {
|
||||||
|
uri,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_remote_directory(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<PathBuf> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::RemoteDirectory))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => {
|
||||||
|
Some(PathBuf::from(x.as_str()))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_local_directory(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<PathBuf> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::LocalDirectory))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => {
|
||||||
|
Some(PathBuf::from(x.as_str()))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_webdav_uri(&self, form_tab: FormTab) -> String {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::WebDAVUri))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) => x,
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_addr(&self, form_tab: FormTab) -> String {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::Address))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) => x,
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_port(&self, form_tab: FormTab) -> u16 {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::Port))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) => {
|
||||||
|
u16::from_str(x.as_str()).unwrap_or_default()
|
||||||
|
}
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_username(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::Username))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_password(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::Password))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_s3_bucket(&self, form_tab: FormTab) -> String {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::S3Bucket))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) => x,
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_s3_region(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::S3Region))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_s3_endpoint(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::S3Endpoint))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_s3_profile(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::S3Profile))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_s3_access_key(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::S3AccessKey))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_s3_secret_access_key(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::S3SecretAccessKey))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_s3_security_token(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::S3SecurityToken))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_s3_session_token(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::S3SessionToken))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_s3_new_path_style(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::S3NewPathStyle)),
|
||||||
|
Ok(State::Single(StateValue::Usize(0)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_kube_namespace(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::KubeNamespace))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_kube_cluster_url(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::KubeClusterUrl))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_kube_username(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::KubeUsername))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_kube_client_cert(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::KubeClientCert))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_kube_client_key(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::KubeClientKey))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_smb_share(&self, form_tab: FormTab) -> String {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::SmbShare))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) => x,
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
pub(in crate::ui::activities::auth) fn get_input_smb_workgroup(
|
||||||
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.app
|
||||||
|
.state(&Self::form_tab_id(form_tab, AuthFormId::SmbWorkgroup))
|
||||||
|
{
|
||||||
|
Ok(State::Single(StateValue::String(x))) => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_new_bookmark(&self) -> (String, bool) {
|
||||||
|
let name = match self.app.state(&Id::BookmarkName) {
|
||||||
|
Ok(State::Single(StateValue::String(name))) => name,
|
||||||
|
_ => String::default(),
|
||||||
|
};
|
||||||
|
if matches!(
|
||||||
|
self.app.state(&Id::BookmarkSavePassword),
|
||||||
|
Ok(State::Single(StateValue::Usize(0)))
|
||||||
|
) {
|
||||||
|
(name, true)
|
||||||
|
} else {
|
||||||
|
(name, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn max_input_mask_size(&self) -> u16 {
|
||||||
|
Self::input_mask_size(self.host_bridge_input_mask())
|
||||||
|
.max(Self::input_mask_size(self.remote_input_mask()))
|
||||||
|
+ 3
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_mask_size(input_mask: InputMask) -> u16 {
|
||||||
|
match input_mask {
|
||||||
|
InputMask::AwsS3
|
||||||
|
| InputMask::Generic
|
||||||
|
| InputMask::Kube
|
||||||
|
| InputMask::Smb
|
||||||
|
| InputMask::WebDAV => 12,
|
||||||
|
InputMask::Localhost => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn fmt_bookmark(
|
||||||
|
name: &str,
|
||||||
|
b: FileTransferParams,
|
||||||
|
) -> String {
|
||||||
|
let addr = Self::fmt_recent(b);
|
||||||
|
format!("{name} ({addr})")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn fmt_recent(b: FileTransferParams) -> String {
|
||||||
|
let protocol = b.protocol.to_string().to_lowercase();
|
||||||
|
match b.params {
|
||||||
|
ProtocolParams::AwsS3(s3) => {
|
||||||
|
let profile = match s3.profile {
|
||||||
|
Some(p) => format!("[{p}]"),
|
||||||
|
None => String::default(),
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"{}://{}{} ({}) {}",
|
||||||
|
protocol,
|
||||||
|
s3.endpoint.unwrap_or_default(),
|
||||||
|
s3.bucket_name,
|
||||||
|
s3.region.as_deref().unwrap_or("custom"),
|
||||||
|
profile
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProtocolParams::Generic(params) => {
|
||||||
|
let username = match params.username {
|
||||||
|
None => String::default(),
|
||||||
|
Some(u) => format!("{u}@"),
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"{}://{}{}:{}",
|
||||||
|
protocol, username, params.address, params.port
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProtocolParams::Kube(params) => {
|
||||||
|
format!(
|
||||||
|
"{}://{}{}",
|
||||||
|
protocol,
|
||||||
|
params
|
||||||
|
.namespace
|
||||||
|
.as_deref()
|
||||||
|
.map(|x| format!("/{x}"))
|
||||||
|
.unwrap_or_else(|| String::from("default")),
|
||||||
|
params
|
||||||
|
.cluster_url
|
||||||
|
.as_deref()
|
||||||
|
.map(|x| format!("@{x}"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[cfg(posix)]
|
||||||
|
ProtocolParams::Smb(params) => {
|
||||||
|
let username = match params.username {
|
||||||
|
None => String::default(),
|
||||||
|
Some(u) => format!("{u}@"),
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"\\\\{username}{}:{}\\{}",
|
||||||
|
params.address, params.port, params.share
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[cfg(win)]
|
||||||
|
ProtocolParams::Smb(params) => {
|
||||||
|
let username = match params.username {
|
||||||
|
None => String::default(),
|
||||||
|
Some(u) => format!("{u}@"),
|
||||||
|
};
|
||||||
|
format!("\\\\{username}{}\\{}", params.address, params.share)
|
||||||
|
}
|
||||||
|
ProtocolParams::WebDAV(params) => params.uri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,512 @@
|
|||||||
|
use tuirealm::subscription::{EventClause, Sub, SubClause};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl AuthActivity {
|
||||||
|
pub(in crate::ui::activities::auth) fn get_host_bridge_generic_params_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::Port),
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
Id::HostBridge(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::HostBridge(AuthFormId::Address),
|
||||||
|
Id::HostBridge(AuthFormId::Port),
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_remote_generic_params_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::Port),
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
Id::Remote(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::Remote(AuthFormId::Address),
|
||||||
|
Id::Remote(AuthFormId::Port),
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_host_bridge_localhost_view(&self) -> [Id; 1] {
|
||||||
|
[Id::HostBridge(AuthFormId::LocalDirectory)]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_host_bridge_s3_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(&Id::HostBridge(AuthFormId::S3AccessKey)) => [
|
||||||
|
Id::HostBridge(AuthFormId::S3Region),
|
||||||
|
Id::HostBridge(AuthFormId::S3Endpoint),
|
||||||
|
Id::HostBridge(AuthFormId::S3Profile),
|
||||||
|
Id::HostBridge(AuthFormId::S3AccessKey),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::S3SecretAccessKey)) => [
|
||||||
|
Id::HostBridge(AuthFormId::S3Endpoint),
|
||||||
|
Id::HostBridge(AuthFormId::S3Profile),
|
||||||
|
Id::HostBridge(AuthFormId::S3AccessKey),
|
||||||
|
Id::HostBridge(AuthFormId::S3SecretAccessKey),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::S3SecurityToken)) => [
|
||||||
|
Id::HostBridge(AuthFormId::S3Profile),
|
||||||
|
Id::HostBridge(AuthFormId::S3AccessKey),
|
||||||
|
Id::HostBridge(AuthFormId::S3SecretAccessKey),
|
||||||
|
Id::HostBridge(AuthFormId::S3SecurityToken),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::S3SessionToken)) => [
|
||||||
|
Id::HostBridge(AuthFormId::S3AccessKey),
|
||||||
|
Id::HostBridge(AuthFormId::S3SecretAccessKey),
|
||||||
|
Id::HostBridge(AuthFormId::S3SecurityToken),
|
||||||
|
Id::HostBridge(AuthFormId::S3SessionToken),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::S3NewPathStyle)) => [
|
||||||
|
Id::HostBridge(AuthFormId::S3SecretAccessKey),
|
||||||
|
Id::HostBridge(AuthFormId::S3SecurityToken),
|
||||||
|
Id::HostBridge(AuthFormId::S3SessionToken),
|
||||||
|
Id::HostBridge(AuthFormId::S3NewPathStyle),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::S3SecurityToken),
|
||||||
|
Id::HostBridge(AuthFormId::S3SessionToken),
|
||||||
|
Id::HostBridge(AuthFormId::S3NewPathStyle),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::S3SessionToken),
|
||||||
|
Id::HostBridge(AuthFormId::S3NewPathStyle),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
Id::HostBridge(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::HostBridge(AuthFormId::S3Bucket),
|
||||||
|
Id::HostBridge(AuthFormId::S3Region),
|
||||||
|
Id::HostBridge(AuthFormId::S3Endpoint),
|
||||||
|
Id::HostBridge(AuthFormId::S3Profile),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_remote_s3_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(&Id::Remote(AuthFormId::S3AccessKey)) => [
|
||||||
|
Id::Remote(AuthFormId::S3Region),
|
||||||
|
Id::Remote(AuthFormId::S3Endpoint),
|
||||||
|
Id::Remote(AuthFormId::S3Profile),
|
||||||
|
Id::Remote(AuthFormId::S3AccessKey),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::S3SecretAccessKey)) => [
|
||||||
|
Id::Remote(AuthFormId::S3Endpoint),
|
||||||
|
Id::Remote(AuthFormId::S3Profile),
|
||||||
|
Id::Remote(AuthFormId::S3AccessKey),
|
||||||
|
Id::Remote(AuthFormId::S3SecretAccessKey),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::S3SecurityToken)) => [
|
||||||
|
Id::Remote(AuthFormId::S3Profile),
|
||||||
|
Id::Remote(AuthFormId::S3AccessKey),
|
||||||
|
Id::Remote(AuthFormId::S3SecretAccessKey),
|
||||||
|
Id::Remote(AuthFormId::S3SecurityToken),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::S3SessionToken)) => [
|
||||||
|
Id::Remote(AuthFormId::S3AccessKey),
|
||||||
|
Id::Remote(AuthFormId::S3SecretAccessKey),
|
||||||
|
Id::Remote(AuthFormId::S3SecurityToken),
|
||||||
|
Id::Remote(AuthFormId::S3SessionToken),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::S3NewPathStyle)) => [
|
||||||
|
Id::Remote(AuthFormId::S3SecretAccessKey),
|
||||||
|
Id::Remote(AuthFormId::S3SecurityToken),
|
||||||
|
Id::Remote(AuthFormId::S3SessionToken),
|
||||||
|
Id::Remote(AuthFormId::S3NewPathStyle),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::S3SecurityToken),
|
||||||
|
Id::Remote(AuthFormId::S3SessionToken),
|
||||||
|
Id::Remote(AuthFormId::S3NewPathStyle),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::S3SessionToken),
|
||||||
|
Id::Remote(AuthFormId::S3NewPathStyle),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
Id::Remote(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::Remote(AuthFormId::S3Bucket),
|
||||||
|
Id::Remote(AuthFormId::S3Region),
|
||||||
|
Id::Remote(AuthFormId::S3Endpoint),
|
||||||
|
Id::Remote(AuthFormId::S3Profile),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_host_bridge_kube_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(&Id::HostBridge(AuthFormId::KubeClientCert)) => [
|
||||||
|
Id::HostBridge(AuthFormId::KubeNamespace),
|
||||||
|
Id::HostBridge(AuthFormId::KubeClusterUrl),
|
||||||
|
Id::HostBridge(AuthFormId::KubeUsername),
|
||||||
|
Id::HostBridge(AuthFormId::KubeClientCert),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::KubeClientKey)) => [
|
||||||
|
Id::HostBridge(AuthFormId::KubeClusterUrl),
|
||||||
|
Id::HostBridge(AuthFormId::KubeUsername),
|
||||||
|
Id::HostBridge(AuthFormId::KubeClientCert),
|
||||||
|
Id::HostBridge(AuthFormId::KubeClientKey),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::KubeUsername),
|
||||||
|
Id::HostBridge(AuthFormId::KubeClientCert),
|
||||||
|
Id::HostBridge(AuthFormId::KubeClientKey),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::KubeClientCert),
|
||||||
|
Id::HostBridge(AuthFormId::KubeClientKey),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
Id::HostBridge(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::HostBridge(AuthFormId::KubeNamespace),
|
||||||
|
Id::HostBridge(AuthFormId::KubeClusterUrl),
|
||||||
|
Id::HostBridge(AuthFormId::KubeUsername),
|
||||||
|
Id::HostBridge(AuthFormId::KubeClientCert),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_remote_kube_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(&Id::Remote(AuthFormId::KubeClientCert)) => [
|
||||||
|
Id::Remote(AuthFormId::KubeNamespace),
|
||||||
|
Id::Remote(AuthFormId::KubeClusterUrl),
|
||||||
|
Id::Remote(AuthFormId::KubeUsername),
|
||||||
|
Id::Remote(AuthFormId::KubeClientCert),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::KubeClientKey)) => [
|
||||||
|
Id::Remote(AuthFormId::KubeClusterUrl),
|
||||||
|
Id::Remote(AuthFormId::KubeUsername),
|
||||||
|
Id::Remote(AuthFormId::KubeClientCert),
|
||||||
|
Id::Remote(AuthFormId::KubeClientKey),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::KubeUsername),
|
||||||
|
Id::Remote(AuthFormId::KubeClientCert),
|
||||||
|
Id::Remote(AuthFormId::KubeClientKey),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::KubeClientCert),
|
||||||
|
Id::Remote(AuthFormId::KubeClientKey),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
Id::Remote(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::Remote(AuthFormId::KubeNamespace),
|
||||||
|
Id::Remote(AuthFormId::KubeClusterUrl),
|
||||||
|
Id::Remote(AuthFormId::KubeUsername),
|
||||||
|
Id::Remote(AuthFormId::KubeClientCert),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
pub(in crate::ui::activities::auth) fn get_host_bridge_smb_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(
|
||||||
|
&Id::HostBridge(AuthFormId::Address)
|
||||||
|
| &Id::HostBridge(AuthFormId::Port)
|
||||||
|
| &Id::HostBridge(AuthFormId::SmbShare)
|
||||||
|
| &Id::HostBridge(AuthFormId::Username),
|
||||||
|
) => [
|
||||||
|
Id::HostBridge(AuthFormId::Address),
|
||||||
|
Id::HostBridge(AuthFormId::Port),
|
||||||
|
Id::HostBridge(AuthFormId::SmbShare),
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::Password)) => [
|
||||||
|
Id::HostBridge(AuthFormId::Port),
|
||||||
|
Id::HostBridge(AuthFormId::SmbShare),
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::SmbWorkgroup)) => [
|
||||||
|
Id::HostBridge(AuthFormId::SmbShare),
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
Id::HostBridge(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::HostBridge(AuthFormId::Address),
|
||||||
|
Id::HostBridge(AuthFormId::Port),
|
||||||
|
Id::HostBridge(AuthFormId::SmbShare),
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(posix)]
|
||||||
|
pub(in crate::ui::activities::auth) fn get_remote_smb_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(
|
||||||
|
&Id::Remote(AuthFormId::Address)
|
||||||
|
| &Id::Remote(AuthFormId::Port)
|
||||||
|
| &Id::Remote(AuthFormId::SmbShare)
|
||||||
|
| &Id::Remote(AuthFormId::Username),
|
||||||
|
) => [
|
||||||
|
Id::Remote(AuthFormId::Address),
|
||||||
|
Id::Remote(AuthFormId::Port),
|
||||||
|
Id::Remote(AuthFormId::SmbShare),
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::Password)) => [
|
||||||
|
Id::Remote(AuthFormId::Port),
|
||||||
|
Id::Remote(AuthFormId::SmbShare),
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::SmbWorkgroup)) => [
|
||||||
|
Id::Remote(AuthFormId::SmbShare),
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
Id::Remote(AuthFormId::SmbWorkgroup),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
Id::Remote(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::Remote(AuthFormId::Address),
|
||||||
|
Id::Remote(AuthFormId::Port),
|
||||||
|
Id::Remote(AuthFormId::SmbShare),
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(win)]
|
||||||
|
pub(in crate::ui::activities::auth) fn get_host_bridge_smb_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(
|
||||||
|
&Id::HostBridge(AuthFormId::Address)
|
||||||
|
| &Id::HostBridge(AuthFormId::Password)
|
||||||
|
| &Id::HostBridge(AuthFormId::SmbShare)
|
||||||
|
| &Id::HostBridge(AuthFormId::Username),
|
||||||
|
) => [
|
||||||
|
Id::HostBridge(AuthFormId::Address),
|
||||||
|
Id::HostBridge(AuthFormId::SmbShare),
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::SmbShare),
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
Id::HostBridge(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::HostBridge(AuthFormId::Address),
|
||||||
|
Id::HostBridge(AuthFormId::SmbShare),
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(win)]
|
||||||
|
pub(in crate::ui::activities::auth) fn get_remote_smb_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(
|
||||||
|
&Id::Remote(AuthFormId::Address)
|
||||||
|
| &Id::Remote(AuthFormId::Password)
|
||||||
|
| &Id::Remote(AuthFormId::SmbShare)
|
||||||
|
| &Id::Remote(AuthFormId::Username),
|
||||||
|
) => [
|
||||||
|
Id::Remote(AuthFormId::Address),
|
||||||
|
Id::Remote(AuthFormId::SmbShare),
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::SmbShare),
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
Some(&Id::Remote(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
Id::Remote(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::Remote(AuthFormId::Address),
|
||||||
|
Id::Remote(AuthFormId::SmbShare),
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_host_bridge_webdav_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
Id::HostBridge(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::HostBridge(AuthFormId::WebDAVUri),
|
||||||
|
Id::HostBridge(AuthFormId::Username),
|
||||||
|
Id::HostBridge(AuthFormId::Password),
|
||||||
|
Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_remote_webdav_view(&self) -> [Id; 4] {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(&Id::Remote(AuthFormId::LocalDirectory)) => [
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
Id::Remote(AuthFormId::LocalDirectory),
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
Id::Remote(AuthFormId::WebDAVUri),
|
||||||
|
Id::Remote(AuthFormId::Username),
|
||||||
|
Id::Remote(AuthFormId::Password),
|
||||||
|
Id::Remote(AuthFormId::RemoteDirectory),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn init_global_listener(&mut self) {
|
||||||
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
if let Err(err) = self.app.mount(
|
||||||
|
Id::GlobalListener,
|
||||||
|
Box::<components::GlobalListener>::default(),
|
||||||
|
vec![
|
||||||
|
Sub::new(
|
||||||
|
EventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Esc,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(
|
||||||
|
EventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Function(10),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(
|
||||||
|
EventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('c'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(
|
||||||
|
EventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('h'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(
|
||||||
|
EventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Function(1),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(
|
||||||
|
EventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('r'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(
|
||||||
|
EventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('s'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(EventClause::WindowResize, SubClause::Always),
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
error!("Failed to mount component: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::auth) fn get_current_form_tab(&self) -> FormTab {
|
||||||
|
match self.app.focus() {
|
||||||
|
Some(&Id::HostBridge(_)) => FormTab::HostBridge,
|
||||||
|
_ => FormTab::Remote,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn no_popup_mounted_clause() -> SubClause<Id> {
|
||||||
|
tuirealm::subclause_and_not!(
|
||||||
|
Id::ErrorPopup,
|
||||||
|
Id::InfoPopup,
|
||||||
|
Id::Keybindings,
|
||||||
|
Id::DeleteBookmarkPopup,
|
||||||
|
Id::DeleteRecentPopup,
|
||||||
|
Id::InstallUpdatePopup,
|
||||||
|
Id::BookmarkSavePassword,
|
||||||
|
Id::WaitPopup
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// This module is split into files, cause it's just too big
|
// This module is split into files, cause it's just too big
|
||||||
mod actions;
|
mod actions;
|
||||||
@@ -20,13 +20,16 @@ use std::time::Duration;
|
|||||||
// Includes
|
// Includes
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use lib::browser;
|
use lib::browser;
|
||||||
use lib::browser::Browser;
|
use lib::browser::{Browser, FileExplorerTab};
|
||||||
|
use lib::pane::Pane;
|
||||||
use lib::transfer::{TransferOpts, TransferStates};
|
use lib::transfer::{TransferOpts, TransferStates};
|
||||||
use lib::walkdir::WalkdirStates;
|
use lib::walkdir::WalkdirStates;
|
||||||
use remotefs::RemoteFs;
|
|
||||||
use session::TransferPayload;
|
use session::TransferPayload;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
use tuirealm::application::Application;
|
||||||
|
use tuirealm::event::NoUserEvent;
|
||||||
|
use tuirealm::listener::EventListenerCfg;
|
||||||
|
use tuirealm::terminal::TerminalAdapter;
|
||||||
|
|
||||||
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
|
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
|
||||||
use crate::config::themes::Theme;
|
use crate::config::themes::Theme;
|
||||||
@@ -34,7 +37,7 @@ use crate::explorer::{FileExplorer, FileSorting};
|
|||||||
use crate::filetransfer::{
|
use crate::filetransfer::{
|
||||||
FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder,
|
FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder,
|
||||||
};
|
};
|
||||||
use crate::host::HostBridge;
|
use crate::host::RemoteBridged;
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
use crate::system::watcher::FsWatcher;
|
use crate::system::watcher::FsWatcher;
|
||||||
|
|
||||||
@@ -67,8 +70,7 @@ enum Id {
|
|||||||
MkdirPopup,
|
MkdirPopup,
|
||||||
NewfilePopup,
|
NewfilePopup,
|
||||||
OpenWithPopup,
|
OpenWithPopup,
|
||||||
ProgressBarFull,
|
TransferProgressBar,
|
||||||
ProgressBarPartial,
|
|
||||||
QuitPopup,
|
QuitPopup,
|
||||||
RenamePopup,
|
RenamePopup,
|
||||||
ReplacePopup,
|
ReplacePopup,
|
||||||
@@ -237,10 +239,6 @@ pub struct FileTransferActivity {
|
|||||||
app: Application<Id, Msg, NoUserEvent>,
|
app: Application<Id, Msg, NoUserEvent>,
|
||||||
/// Whether should redraw UI
|
/// Whether should redraw UI
|
||||||
redraw: bool,
|
redraw: bool,
|
||||||
/// Host bridge
|
|
||||||
host_bridge: Box<dyn HostBridge>,
|
|
||||||
/// Remote host client
|
|
||||||
client: Box<dyn RemoteFs>,
|
|
||||||
/// Browser
|
/// Browser
|
||||||
browser: Browser,
|
browser: Browser,
|
||||||
/// Current log lines
|
/// Current log lines
|
||||||
@@ -253,10 +251,6 @@ pub struct FileTransferActivity {
|
|||||||
cache: Option<TempDir>,
|
cache: Option<TempDir>,
|
||||||
/// Fs watcher
|
/// Fs watcher
|
||||||
fswatcher: Option<FsWatcher>,
|
fswatcher: Option<FsWatcher>,
|
||||||
/// host bridge connected
|
|
||||||
host_bridge_connected: bool,
|
|
||||||
/// remote connected once
|
|
||||||
remote_connected: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -272,23 +266,34 @@ impl FileTransferActivity {
|
|||||||
let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client)?;
|
let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client)?;
|
||||||
let host_bridge_connected = host_bridge.is_localhost();
|
let host_bridge_connected = host_bridge.is_localhost();
|
||||||
let enable_fs_watcher = host_bridge.is_localhost();
|
let enable_fs_watcher = host_bridge.is_localhost();
|
||||||
|
// Build remote client, wrapped as HostBridge via RemoteBridged
|
||||||
|
let remote_client = RemoteFsBuilder::build(
|
||||||
|
remote_params.protocol,
|
||||||
|
remote_params.params.clone(),
|
||||||
|
&config_client,
|
||||||
|
)?;
|
||||||
|
let remote_fs: Box<dyn crate::host::HostBridge> =
|
||||||
|
Box::new(RemoteBridged::from(remote_client));
|
||||||
|
// Build panes
|
||||||
|
let local_pane = Pane::new(
|
||||||
|
Browser::build_local_explorer(&config_client),
|
||||||
|
host_bridge_connected,
|
||||||
|
host_bridge,
|
||||||
|
);
|
||||||
|
let remote_pane = Pane::new(
|
||||||
|
Browser::build_remote_explorer(&config_client),
|
||||||
|
false,
|
||||||
|
remote_fs,
|
||||||
|
);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
exit_reason: None,
|
exit_reason: None,
|
||||||
context: None,
|
context: None,
|
||||||
app: Application::init(
|
app: Application::init(
|
||||||
EventListenerCfg::default()
|
EventListenerCfg::default().crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
||||||
.poll_timeout(ticks)
|
|
||||||
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
|
||||||
),
|
),
|
||||||
redraw: true,
|
redraw: true,
|
||||||
host_bridge,
|
browser: Browser::new(local_pane, remote_pane),
|
||||||
client: RemoteFsBuilder::build(
|
log_records: VecDeque::with_capacity(256),
|
||||||
remote_params.protocol,
|
|
||||||
remote_params.params.clone(),
|
|
||||||
&config_client,
|
|
||||||
)?,
|
|
||||||
browser: Browser::new(&config_client),
|
|
||||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
|
||||||
walkdir: WalkdirStates::default(),
|
walkdir: WalkdirStates::default(),
|
||||||
transfer: TransferStates::default(),
|
transfer: TransferStates::default(),
|
||||||
cache: TempDir::new().ok(),
|
cache: TempDir::new().ok(),
|
||||||
@@ -297,11 +302,18 @@ impl FileTransferActivity {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
host_bridge_connected,
|
|
||||||
remote_connected: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the active tab targets the local side
|
||||||
|
/// (either the main host-bridge pane or a find-result rooted there).
|
||||||
|
fn is_local_tab(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.browser.tab(),
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn host_bridge(&self) -> &FileExplorer {
|
fn host_bridge(&self) -> &FileExplorer {
|
||||||
self.browser.host_bridge()
|
self.browser.host_bridge()
|
||||||
}
|
}
|
||||||
@@ -433,7 +445,7 @@ impl Activity for FileTransferActivity {
|
|||||||
error!("Failed to enter raw mode: {}", err);
|
error!("Failed to enter raw mode: {}", err);
|
||||||
}
|
}
|
||||||
// Get files at current pwd
|
// Get files at current pwd
|
||||||
if self.host_bridge.is_localhost() {
|
if self.browser.local_pane().fs.is_localhost() {
|
||||||
debug!("Reloading host bridge directory");
|
debug!("Reloading host bridge directory");
|
||||||
self.reload_host_bridge_dir();
|
self.reload_host_bridge_dir();
|
||||||
}
|
}
|
||||||
@@ -460,14 +472,16 @@ impl Activity for FileTransferActivity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check if connected to host bridge (popup must be None, otherwise would try reconnecting in loop in case of error)
|
// Check if connected to host bridge (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||||
if (!self.host_bridge.is_connected() || !self.host_bridge_connected)
|
if (!self.browser.local_pane_mut().fs.is_connected()
|
||||||
|
|| !self.browser.local_pane().connected)
|
||||||
&& !self.app.mounted(&Id::FatalPopup)
|
&& !self.app.mounted(&Id::FatalPopup)
|
||||||
&& !self.host_bridge.is_localhost()
|
&& !self.browser.local_pane().fs.is_localhost()
|
||||||
{
|
{
|
||||||
let host_bridge_params = self.context().host_bridge_params().unwrap();
|
let host_bridge_params = self.context().host_bridge_params().unwrap();
|
||||||
let ft_params = host_bridge_params.unwrap_protocol_params();
|
let msg: String = match host_bridge_params.protocol_params() {
|
||||||
// print params
|
Some(ft_params) => Self::get_connection_msg(ft_params),
|
||||||
let msg: String = Self::get_connection_msg(ft_params);
|
None => String::from("Connecting..."),
|
||||||
|
};
|
||||||
// Set init state to connecting popup
|
// Set init state to connecting popup
|
||||||
self.mount_blocking_wait(msg.as_str());
|
self.mount_blocking_wait(msg.as_str());
|
||||||
// Connect to remote
|
// Connect to remote
|
||||||
@@ -476,9 +490,10 @@ impl Activity for FileTransferActivity {
|
|||||||
self.redraw = true;
|
self.redraw = true;
|
||||||
}
|
}
|
||||||
// Check if connected to remote (popup must be None, otherwise would try reconnecting in loop in case of error)
|
// Check if connected to remote (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||||
if (!self.client.is_connected() || !self.remote_connected)
|
if (!self.browser.remote_pane_mut().fs.is_connected()
|
||||||
|
|| !self.browser.remote_pane().connected)
|
||||||
&& !self.app.mounted(&Id::FatalPopup)
|
&& !self.app.mounted(&Id::FatalPopup)
|
||||||
&& self.host_bridge.is_connected()
|
&& self.browser.local_pane_mut().fs.is_connected()
|
||||||
{
|
{
|
||||||
let ftparams = self.context().remote_params().unwrap();
|
let ftparams = self.context().remote_params().unwrap();
|
||||||
// print params
|
// print params
|
||||||
@@ -522,14 +537,21 @@ impl Activity for FileTransferActivity {
|
|||||||
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
||||||
error!("Failed to clear screen: {}", err);
|
error!("Failed to clear screen: {}", err);
|
||||||
}
|
}
|
||||||
// Disconnect client
|
// Disconnect remote
|
||||||
if self.client.is_connected() {
|
if self.browser.remote_pane_mut().fs.is_connected() {
|
||||||
let _ = self.client.disconnect();
|
let _ = self.browser.remote_pane_mut().fs.disconnect();
|
||||||
}
|
}
|
||||||
// disconnect host bridge
|
// Disconnect host bridge
|
||||||
if self.host_bridge.is_connected() {
|
if self.browser.local_pane_mut().fs.is_connected() {
|
||||||
let _ = self.host_bridge.disconnect();
|
let _ = self.browser.local_pane_mut().fs.disconnect();
|
||||||
}
|
}
|
||||||
self.context.take()
|
self.context.take()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Log a UI operation error instead of panicking.
|
||||||
|
fn ui_result<T>(result: Result<T, impl std::fmt::Display>) {
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("UI operation failed: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
-28
@@ -1,12 +1,12 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
use remotefs::fs::UnixPex;
|
use remotefs::fs::UnixPex;
|
||||||
use tuirealm::{State, StateValue};
|
use tuirealm::state::{State, StateValue};
|
||||||
|
|
||||||
use super::browser::FileExplorerTab;
|
use super::browser::FileExplorerTab;
|
||||||
use super::lib::browser::FoundExplorerTab;
|
use super::lib::browser::FoundExplorerTab;
|
||||||
@@ -92,43 +92,50 @@ impl FileTransferActivity {
|
|||||||
self.get_selected_files(&Id::ExplorerHostBridge)
|
self.get_selected_files(&Id::ExplorerHostBridge)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_local_selected_file(&self) -> Option<File> {
|
|
||||||
self.get_selected_file(&Id::ExplorerHostBridge)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get remote file entry
|
/// Get remote file entry
|
||||||
pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile {
|
pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile {
|
||||||
self.get_selected_files(&Id::ExplorerRemote)
|
self.get_selected_files(&Id::ExplorerRemote)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_remote_selected_file(&self) -> Option<File> {
|
|
||||||
self.get_selected_file(&Id::ExplorerRemote)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether only one entry is selected on local host
|
|
||||||
pub(crate) fn is_local_selected_one(&mut self) -> bool {
|
|
||||||
matches!(self.get_local_selected_entries(), SelectedFile::One(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether only one entry is selected on remote host
|
|
||||||
pub(crate) fn is_remote_selected_one(&mut self) -> bool {
|
|
||||||
matches!(self.get_remote_selected_entries(), SelectedFile::One(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get remote file entry
|
/// Get remote file entry
|
||||||
pub(crate) fn get_found_selected_entries(&mut self) -> SelectedFile {
|
pub(crate) fn get_found_selected_entries(&mut self) -> SelectedFile {
|
||||||
self.get_selected_files(&Id::ExplorerFind)
|
self.get_selected_files(&Id::ExplorerFind)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_found_selected_file(&self) -> Option<File> {
|
pub(crate) fn get_found_selected_file(&self) -> Option<File> {
|
||||||
self.get_selected_file(&Id::ExplorerFind)
|
self.get_selected_file_by_id(&Id::ExplorerFind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get selected entries from whichever tab is currently active.
|
||||||
|
pub(crate) fn get_selected_entries(&mut self) -> SelectedFile {
|
||||||
|
let id = match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge => Id::ExplorerHostBridge,
|
||||||
|
FileExplorerTab::Remote => Id::ExplorerRemote,
|
||||||
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => Id::ExplorerFind,
|
||||||
|
};
|
||||||
|
self.get_selected_files(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get single selected file from whichever tab is currently active.
|
||||||
|
pub(crate) fn get_selected_file(&self) -> Option<File> {
|
||||||
|
let id = match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge => Id::ExplorerHostBridge,
|
||||||
|
FileExplorerTab::Remote => Id::ExplorerRemote,
|
||||||
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => Id::ExplorerFind,
|
||||||
|
};
|
||||||
|
self.get_selected_file_by_id(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether only one entry is selected on the current tab.
|
||||||
|
pub(crate) fn is_selected_one(&mut self) -> bool {
|
||||||
|
matches!(self.get_selected_entries(), SelectedFile::One(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- private
|
// -- private
|
||||||
|
|
||||||
fn get_selected_index(&self, id: &Id) -> SelectedFileIndex {
|
fn get_selected_index(&self, id: &Id) -> SelectedFileIndex {
|
||||||
match self.app.state(id) {
|
match self.app.state(id) {
|
||||||
Ok(State::One(StateValue::Usize(idx))) => SelectedFileIndex::One(idx),
|
Ok(State::Single(StateValue::Usize(idx))) => SelectedFileIndex::One(idx),
|
||||||
_ => SelectedFileIndex::None,
|
_ => SelectedFileIndex::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,13 +171,13 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
fn get_file_from_path(&mut self, id: &Id, path: &Path) -> Option<File> {
|
fn get_file_from_path(&mut self, id: &Id, path: &Path) -> Option<File> {
|
||||||
match *id {
|
match *id {
|
||||||
Id::ExplorerHostBridge => self.host_bridge.stat(path).ok(),
|
Id::ExplorerHostBridge => self.browser.local_pane_mut().fs.stat(path).ok(),
|
||||||
Id::ExplorerRemote => self.client.stat(path).ok(),
|
Id::ExplorerRemote => self.browser.remote_pane_mut().fs.stat(path).ok(),
|
||||||
Id::ExplorerFind => {
|
Id::ExplorerFind => {
|
||||||
let found = self.browser.found_tab().unwrap();
|
let found = self.browser.found_tab().unwrap();
|
||||||
match found {
|
match found {
|
||||||
FoundExplorerTab::Local => self.host_bridge.stat(path).ok(),
|
FoundExplorerTab::Local => self.browser.local_pane_mut().fs.stat(path).ok(),
|
||||||
FoundExplorerTab::Remote => self.client.stat(path).ok(),
|
FoundExplorerTab::Remote => self.browser.remote_pane_mut().fs.stat(path).ok(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -182,11 +189,16 @@ impl FileTransferActivity {
|
|||||||
Id::ExplorerHostBridge => self.host_bridge(),
|
Id::ExplorerHostBridge => self.host_bridge(),
|
||||||
Id::ExplorerRemote => self.remote(),
|
Id::ExplorerRemote => self.remote(),
|
||||||
Id::ExplorerFind => self.found().as_ref().unwrap(),
|
Id::ExplorerFind => self.found().as_ref().unwrap(),
|
||||||
_ => unreachable!(),
|
_ => {
|
||||||
|
error!(
|
||||||
|
"browser_by_id called with unexpected id: {id:?}; falling back to local explorer"
|
||||||
|
);
|
||||||
|
self.host_bridge()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_selected_file(&self, id: &Id) -> Option<File> {
|
fn get_selected_file_by_id(&self, id: &Id) -> Option<File> {
|
||||||
let browser = self.browser_by_id(id);
|
let browser = self.browser_by_id(id);
|
||||||
// if no transfer queue, return selected files
|
// if no transfer queue, return selected files
|
||||||
match self.get_selected_index(id) {
|
match self.get_selected_index(id) {
|
||||||
@@ -1,63 +1,45 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
|
|
||||||
use super::{FileExplorerTab, FileTransferActivity, LogLevel, Msg, PendingActionMsg};
|
use super::{FileTransferActivity, LogLevel, Msg, PendingActionMsg};
|
||||||
|
|
||||||
/// Describes destination for sync browsing
|
/// Describes destination for sync browsing
|
||||||
enum SyncBrowsingDestination {
|
pub(crate) enum SyncBrowsingDestination {
|
||||||
Path(String),
|
Path(String),
|
||||||
ParentDir,
|
ParentDir,
|
||||||
PreviousDir,
|
PreviousDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// Enter a directory on local host from entry
|
/// Enter a directory from entry, dispatching via the active tab's pane.
|
||||||
pub(crate) fn action_enter_local_dir(&mut self, dir: File) {
|
pub(crate) fn action_enter_dir(&mut self, dir: File) {
|
||||||
self.host_bridge_changedir(dir.path(), true);
|
self.pane_changedir(dir.path(), true);
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enter a directory on local host from entry
|
/// Change directory reading value from input, dispatching via the active tab's pane.
|
||||||
pub(crate) fn action_enter_remote_dir(&mut self, dir: File) {
|
pub(crate) fn action_change_dir(&mut self, input: String) {
|
||||||
self.remote_changedir(dir.path(), true);
|
let dir_path = self.pane_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
self.pane_changedir(dir_path.as_path(), true);
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change local directory reading value from input
|
|
||||||
pub(crate) fn action_change_local_dir(&mut self, input: String) {
|
|
||||||
let dir_path: PathBuf =
|
|
||||||
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
|
||||||
self.host_bridge_changedir(dir_path.as_path(), true);
|
|
||||||
// Check whether to sync
|
// Check whether to sync
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change remote directory reading value from input
|
/// Go to previous directory, dispatching via the active tab's pane.
|
||||||
pub(crate) fn action_change_remote_dir(&mut self, input: String) {
|
pub(crate) fn action_go_to_previous_dir(&mut self) {
|
||||||
let dir_path: PathBuf = self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
let prev = self.browser.fs_pane_mut().explorer.popd();
|
||||||
self.remote_changedir(dir_path.as_path(), true);
|
if let Some(d) = prev {
|
||||||
// Check whether to sync
|
self.pane_changedir(d.as_path(), false);
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Go to previous directory from localhost
|
|
||||||
pub(crate) fn action_go_to_previous_local_dir(&mut self) {
|
|
||||||
if let Some(d) = self.host_bridge_mut().popd() {
|
|
||||||
self.host_bridge_changedir(d.as_path(), false);
|
|
||||||
// Check whether to sync
|
// Check whether to sync
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
||||||
@@ -65,41 +47,12 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Go to previous directory from remote host
|
/// Go to upper directory, dispatching via the active tab's pane.
|
||||||
pub(crate) fn action_go_to_previous_remote_dir(&mut self) {
|
pub(crate) fn action_go_to_upper_dir(&mut self) {
|
||||||
if let Some(d) = self.remote_mut().popd() {
|
let path = self.browser.fs_pane().explorer.wrkdir.clone();
|
||||||
self.remote_changedir(d.as_path(), false);
|
|
||||||
// Check whether to sync
|
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Go to upper directory on local host
|
|
||||||
pub(crate) fn action_go_to_local_upper_dir(&mut self) {
|
|
||||||
// Get pwd
|
|
||||||
let path: PathBuf = self.host_bridge().wrkdir.clone();
|
|
||||||
// Go to parent directory
|
|
||||||
if let Some(parent) = path.as_path().parent() {
|
if let Some(parent) = path.as_path().parent() {
|
||||||
self.host_bridge_changedir(parent, true);
|
self.pane_changedir(parent, true);
|
||||||
// If sync is enabled update remote too
|
// If sync is enabled update the other side too
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// #### action_go_to_remote_upper_dir
|
|
||||||
///
|
|
||||||
/// Go to upper directory on remote host
|
|
||||||
pub(crate) fn action_go_to_remote_upper_dir(&mut self) {
|
|
||||||
// Get pwd
|
|
||||||
let path: PathBuf = self.remote().wrkdir.clone();
|
|
||||||
// Go to parent directory
|
|
||||||
if let Some(parent) = path.as_path().parent() {
|
|
||||||
self.remote_changedir(parent, true);
|
|
||||||
// If sync is enabled update local too
|
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
||||||
}
|
}
|
||||||
@@ -110,16 +63,18 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
/// Synchronize browsing on the target browser.
|
/// Synchronize browsing on the target browser.
|
||||||
/// If destination doesn't exist, then prompt for directory creation.
|
/// If destination doesn't exist, then prompt for directory creation.
|
||||||
fn synchronize_browsing(&mut self, destination: SyncBrowsingDestination) {
|
pub(crate) fn synchronize_browsing(&mut self, destination: SyncBrowsingDestination) {
|
||||||
// Get destination path
|
// Get destination path
|
||||||
let path = match self.resolve_sync_browsing_destination(&destination) {
|
let path = match self.resolve_sync_browsing_destination(&destination) {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
trace!("Synchronizing browsing to path {}", path.display());
|
trace!("Synchronizing browsing to path {}", path.display());
|
||||||
// Check whether destination exists on host
|
// Check whether destination exists on the opposite side
|
||||||
let exists = match self.browser.tab() {
|
let is_local = self.is_local_tab();
|
||||||
FileExplorerTab::HostBridge => match self.client.exists(path.as_path()) {
|
let exists = if is_local {
|
||||||
|
// Current tab is local, so the opposite is remote
|
||||||
|
match self.browser.remote_pane_mut().fs.exists(path.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -129,8 +84,10 @@ impl FileTransferActivity {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
FileExplorerTab::Remote => match self.host_bridge.exists(path.as_path()) {
|
} else {
|
||||||
|
// Current tab is remote, so the opposite is local
|
||||||
|
match self.browser.local_pane_mut().fs.exists(path.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -140,8 +97,7 @@ impl FileTransferActivity {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => return,
|
|
||||||
};
|
};
|
||||||
let name = path
|
let name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -159,12 +115,8 @@ impl FileTransferActivity {
|
|||||||
]) == Msg::PendingAction(PendingActionMsg::MakePendingDirectory)
|
]) == Msg::PendingAction(PendingActionMsg::MakePendingDirectory)
|
||||||
{
|
{
|
||||||
trace!("User wants to create the unexisting directory");
|
trace!("User wants to create the unexisting directory");
|
||||||
// Make directory
|
// Make directory on the opposite side
|
||||||
match self.browser.tab() {
|
self.sync_mkdir_on_opposite(name.clone());
|
||||||
FileExplorerTab::HostBridge => self.action_remote_mkdir(name.clone()),
|
|
||||||
FileExplorerTab::Remote => self.action_local_mkdir(name.clone()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Do not synchronize, disable sync browsing and return
|
// Do not synchronize, disable sync browsing and return
|
||||||
trace!(
|
trace!(
|
||||||
@@ -183,23 +135,21 @@ impl FileTransferActivity {
|
|||||||
self.umount_sync_browsing_mkdir_popup();
|
self.umount_sync_browsing_mkdir_popup();
|
||||||
}
|
}
|
||||||
trace!("Entering on the other explorer directory {}", name);
|
trace!("Entering on the other explorer directory {}", name);
|
||||||
// Enter directory
|
// Enter directory on the opposite side
|
||||||
match destination {
|
let push = !matches!(destination, SyncBrowsingDestination::PreviousDir);
|
||||||
SyncBrowsingDestination::ParentDir => match self.browser.tab() {
|
if is_local {
|
||||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
self.remote_changedir(path.as_path(), push);
|
||||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
} else {
|
||||||
_ => {}
|
self.local_changedir(path.as_path(), push);
|
||||||
},
|
}
|
||||||
SyncBrowsingDestination::Path(_) => match self.browser.tab() {
|
}
|
||||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
|
||||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
/// Create a directory on the opposite side for sync browsing.
|
||||||
_ => {}
|
fn sync_mkdir_on_opposite(&mut self, name: String) {
|
||||||
},
|
if self.is_local_tab() {
|
||||||
SyncBrowsingDestination::PreviousDir => match self.browser.tab() {
|
self.action_remote_mkdir(name);
|
||||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), false),
|
} else {
|
||||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), false),
|
self.action_local_mkdir(name);
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,35 +158,34 @@ impl FileTransferActivity {
|
|||||||
&mut self,
|
&mut self,
|
||||||
destination: &SyncBrowsingDestination,
|
destination: &SyncBrowsingDestination,
|
||||||
) -> Option<PathBuf> {
|
) -> Option<PathBuf> {
|
||||||
match (destination, self.browser.tab()) {
|
let is_local = self.is_local_tab();
|
||||||
// NOTE: tab and methods are switched on purpose
|
match destination {
|
||||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::HostBridge) => {
|
// NOTE: tab and methods are switched on purpose (we resolve from the opposite side)
|
||||||
self.remote().wrkdir.parent().map(|x| x.to_path_buf())
|
SyncBrowsingDestination::ParentDir => {
|
||||||
}
|
if is_local {
|
||||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => {
|
self.remote().wrkdir.parent().map(Path::to_path_buf)
|
||||||
self.host_bridge().wrkdir.parent().map(|x| x.to_path_buf())
|
|
||||||
}
|
|
||||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::HostBridge) => {
|
|
||||||
if let Some(p) = self.remote_mut().popd() {
|
|
||||||
Some(p)
|
|
||||||
} else {
|
} else {
|
||||||
warn!("Cannot synchronize browsing: remote has no previous directory in stack");
|
self.host_bridge().wrkdir.parent().map(Path::to_path_buf)
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => {
|
SyncBrowsingDestination::PreviousDir => {
|
||||||
if let Some(p) = self.host_bridge_mut().popd() {
|
if is_local {
|
||||||
|
if let Some(p) = self.remote_mut().popd() {
|
||||||
|
Some(p)
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Cannot synchronize browsing: remote has no previous directory in stack"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else if let Some(p) = self.host_bridge_mut().popd() {
|
||||||
Some(p)
|
Some(p)
|
||||||
} else {
|
} else {
|
||||||
warn!("Cannot synchronize browsing: local has no previous directory in stack");
|
warn!("Cannot synchronize browsing: local has no previous directory in stack");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(SyncBrowsingDestination::Path(p), _) => Some(PathBuf::from(p.as_str())),
|
SyncBrowsingDestination::Path(p) => Some(PathBuf::from(p.as_str())),
|
||||||
_ => {
|
|
||||||
warn!("Cannot synchronize browsing for current explorer");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,83 +3,12 @@ use remotefs::fs::UnixPex;
|
|||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub fn action_local_chmod(&mut self, mode: UnixPex) {
|
/// Change file mode for the currently selected entries via the active tab's pane.
|
||||||
let files = self.get_local_selected_entries().get_files();
|
pub(crate) fn action_chmod(&mut self, mode: UnixPex) {
|
||||||
|
let files = self.get_selected_entries().get_files();
|
||||||
|
|
||||||
for file in files {
|
for file in files {
|
||||||
if let Err(err) = self.host_bridge.chmod(file.path(), mode) {
|
if let Err(err) = self.browser.fs_pane_mut().fs.chmod(file.path(), mode) {
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"could not change mode for {}: {}",
|
|
||||||
file.path().display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action_remote_chmod(&mut self, mode: UnixPex) {
|
|
||||||
let files = self.get_remote_selected_entries().get_files();
|
|
||||||
|
|
||||||
for file in files {
|
|
||||||
let mut metadata = file.metadata.clone();
|
|
||||||
metadata.mode = Some(mode);
|
|
||||||
|
|
||||||
if let Err(err) = self.client.setstat(file.path(), metadata) {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"could not change mode for {}: {}",
|
|
||||||
file.path().display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action_find_local_chmod(&mut self, mode: UnixPex) {
|
|
||||||
let files = self.get_found_selected_entries().get_files();
|
|
||||||
|
|
||||||
for file in files {
|
|
||||||
if let Err(err) = self.host_bridge.chmod(file.path(), mode) {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"could not change mode for {}: {}",
|
|
||||||
file.path().display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action_find_remote_chmod(&mut self, mode: UnixPex) {
|
|
||||||
let files = self.get_found_selected_entries().get_files();
|
|
||||||
|
|
||||||
for file in files {
|
|
||||||
let mut metadata = file.metadata.clone();
|
|
||||||
metadata.mode = Some(mode);
|
|
||||||
|
|
||||||
if let Err(err) = self.client.setstat(file.path(), metadata) {
|
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -1,61 +1,38 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use remotefs::{File, RemoteErrorType};
|
use remotefs::File;
|
||||||
|
|
||||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// Copy file on local
|
/// Copy the currently selected file(s) via the active tab's pane.
|
||||||
pub(crate) fn action_local_copy(&mut self, input: String) {
|
pub(crate) fn action_copy(&mut self, input: String) {
|
||||||
match self.get_local_selected_entries() {
|
match self.get_selected_entries() {
|
||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
let dest_path: PathBuf = PathBuf::from(input);
|
let dest_path = PathBuf::from(input);
|
||||||
self.local_copy_file(&entry, dest_path.as_path());
|
self.copy_file(entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
|
||||||
for (entry, mut dest_path) in entries.into_iter() {
|
for (entry, mut dest_path) in entries.into_iter() {
|
||||||
dest_path.push(entry.name());
|
dest_path.push(entry.name());
|
||||||
self.local_copy_file(&entry, dest_path.as_path());
|
self.copy_file(entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear selection
|
// clear selection and reload
|
||||||
self.host_bridge_mut().clear_queue();
|
self.browser.explorer_mut().clear_queue();
|
||||||
self.reload_host_bridge_filelist();
|
self.reload_browser_file_list();
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy file on remote
|
fn copy_file(&mut self, entry: File, dest: &Path) {
|
||||||
pub(crate) fn action_remote_copy(&mut self, input: String) {
|
match self.browser.fs_pane_mut().fs.copy(&entry, dest) {
|
||||||
match self.get_remote_selected_entries() {
|
|
||||||
SelectedFile::One(entry) => {
|
|
||||||
let dest_path: PathBuf = PathBuf::from(input);
|
|
||||||
self.remote_copy_file(entry, dest_path.as_path());
|
|
||||||
}
|
|
||||||
SelectedFile::Many(entries) => {
|
|
||||||
// Iter files
|
|
||||||
for (entry, mut dest_path) in entries.into_iter() {
|
|
||||||
dest_path.push(entry.name());
|
|
||||||
self.remote_copy_file(entry, dest_path.as_path());
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear selection
|
|
||||||
self.remote_mut().clear_queue();
|
|
||||||
self.reload_remote_filelist();
|
|
||||||
}
|
|
||||||
SelectedFile::None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn local_copy_file(&mut self, entry: &File, dest: &Path) {
|
|
||||||
match self.host_bridge.copy(entry, dest) {
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
@@ -66,45 +43,23 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => self.log_and_alert(
|
Err(err) => {
|
||||||
LogLevel::Error,
|
// On remote tabs, fall back to tricky_copy (download + re-upload)
|
||||||
format!(
|
// when the protocol doesn't support server-side copy.
|
||||||
"Could not copy \"{}\" to \"{}\": {}",
|
if !self.is_local_tab() {
|
||||||
entry.path().display(),
|
|
||||||
dest.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remote_copy_file(&mut self, entry: File, dest: &Path) {
|
|
||||||
match self.client.as_mut().copy(entry.path(), dest) {
|
|
||||||
Ok(_) => {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!(
|
|
||||||
"Copied \"{}\" to \"{}\"",
|
|
||||||
entry.path().display(),
|
|
||||||
dest.display()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => match err.kind {
|
|
||||||
RemoteErrorType::UnsupportedFeature => {
|
|
||||||
// If copy is not supported, perform the tricky copy
|
|
||||||
let _ = self.tricky_copy(entry, dest);
|
let _ = self.tricky_copy(entry, dest);
|
||||||
|
} else {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not copy \"{}\" to \"{}\": {}",
|
||||||
|
entry.path().display(),
|
||||||
|
dest.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_ => self.log_and_alert(
|
}
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not copy \"{}\" to \"{}\": {}",
|
|
||||||
entry.path().display(),
|
|
||||||
dest.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +93,12 @@ impl FileTransferActivity {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
// Stat dir
|
// Stat dir
|
||||||
let tempdir_entry = match self.host_bridge.stat(tempdir_path.as_path()) {
|
let tempdir_entry = match self
|
||||||
|
.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.stat(tempdir_path.as_path())
|
||||||
|
{
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
@@ -191,9 +151,17 @@ impl FileTransferActivity {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry = match self.host_bridge.stat(tmpfile.path()) {
|
let tmpfile_entry = match self.browser.local_pane_mut().fs.stat(tmpfile.path()) {
|
||||||
Ok(e) if e.is_file() => e,
|
Ok(e) if e.is_file() => e,
|
||||||
Ok(_) => panic!("{} is not a file", tmpfile.path().display()),
|
Ok(_) => {
|
||||||
|
let msg = format!(
|
||||||
|
"Copy failed: \"{}\" is not a file",
|
||||||
|
tmpfile.path().display()
|
||||||
|
);
|
||||||
|
error!("{msg}");
|
||||||
|
self.log_and_alert(LogLevel::Error, msg.clone());
|
||||||
|
return Err(msg);
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
@@ -8,72 +8,28 @@ use remotefs::File;
|
|||||||
use super::{FileTransferActivity, LogLevel, SelectedFile};
|
use super::{FileTransferActivity, LogLevel, SelectedFile};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_delete(&mut self) {
|
/// Delete the currently selected file(s) via the active tab's pane.
|
||||||
match self.get_local_selected_entries() {
|
pub(crate) fn action_delete(&mut self) {
|
||||||
|
match self.get_selected_entries() {
|
||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
// Delete file
|
self.remove_file(&entry);
|
||||||
self.local_remove_file(&entry);
|
|
||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
|
||||||
for (entry, _) in entries.iter() {
|
for (entry, _) in entries.iter() {
|
||||||
// Delete file
|
self.remove_file(entry);
|
||||||
self.local_remove_file(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear selection
|
// clear selection and reload
|
||||||
self.host_bridge_mut().clear_queue();
|
self.browser.explorer_mut().clear_queue();
|
||||||
self.reload_host_bridge_filelist();
|
self.reload_browser_file_list();
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_delete(&mut self) {
|
/// Remove a single file or directory via the active tab's pane.
|
||||||
match self.get_remote_selected_entries() {
|
pub(crate) fn remove_file(&mut self, entry: &File) {
|
||||||
SelectedFile::One(entry) => {
|
match self.browser.fs_pane_mut().fs.remove(entry) {
|
||||||
// Delete file
|
|
||||||
self.remote_remove_file(&entry);
|
|
||||||
}
|
|
||||||
SelectedFile::Many(entries) => {
|
|
||||||
// Iter files
|
|
||||||
for (entry, _) in entries.iter() {
|
|
||||||
// Delete file
|
|
||||||
self.remote_remove_file(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear selection
|
|
||||||
self.remote_mut().clear_queue();
|
|
||||||
self.reload_remote_filelist();
|
|
||||||
}
|
|
||||||
SelectedFile::None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn local_remove_file(&mut self, entry: &File) {
|
|
||||||
match self.host_bridge.remove(entry) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Log
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Removed file \"{}\"", entry.path().display()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not delete file \"{}\": {}",
|
|
||||||
entry.path().display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn remote_remove_file(&mut self, entry: &File) {
|
|
||||||
match self.client.remove_dir_all(entry.path()) {
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
@@ -9,7 +9,9 @@ use std::time::SystemTime;
|
|||||||
|
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
use remotefs::fs::Metadata;
|
use remotefs::fs::Metadata;
|
||||||
|
use tuirealm::terminal::TerminalAdapter;
|
||||||
|
|
||||||
|
use super::super::ui_result;
|
||||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -28,7 +30,7 @@ impl FileTransferActivity {
|
|||||||
format!("Opening file \"{}\"…", entry.path().display()),
|
format!("Opening file \"{}\"…", entry.path().display()),
|
||||||
);
|
);
|
||||||
// Edit file
|
// Edit file
|
||||||
let res = match self.host_bridge.is_localhost() {
|
let res = match self.browser.local_pane().fs.is_localhost() {
|
||||||
true => self.edit_local_file(entry.path()).map(|_| ()),
|
true => self.edit_local_file(entry.path()).map(|_| ()),
|
||||||
false => self.edit_bridged_local_file(entry),
|
false => self.edit_bridged_local_file(entry),
|
||||||
};
|
};
|
||||||
@@ -88,7 +90,7 @@ impl FileTransferActivity {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// open from host bridge
|
// open from host bridge
|
||||||
let mut reader = match self.host_bridge.open_file(entry.path()) {
|
let mut reader = match self.browser.local_pane_mut().fs.open_file(entry.path()) {
|
||||||
Ok(reader) => reader,
|
Ok(reader) => reader,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!("Failed to open bridged entry: {err}"));
|
return Err(format!("Failed to open bridged entry: {err}"));
|
||||||
@@ -122,7 +124,7 @@ impl FileTransferActivity {
|
|||||||
return Err(format!("Could not open file: {err}"));
|
return Err(format!("Could not open file: {err}"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut writer = match self.host_bridge.create_file(
|
let mut writer = match self.browser.local_pane_mut().fs.create_file(
|
||||||
entry.path(),
|
entry.path(),
|
||||||
&Metadata {
|
&Metadata {
|
||||||
size: new_file_size,
|
size: new_file_size,
|
||||||
@@ -139,7 +141,9 @@ impl FileTransferActivity {
|
|||||||
return Err(format!("Could not write file: {err}"));
|
return Err(format!("Could not write file: {err}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.host_bridge
|
self.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
.finalize_write(writer)
|
.finalize_write(writer)
|
||||||
.map_err(|err| format!("Could not write file: {err}"))?;
|
.map_err(|err| format!("Could not write file: {err}"))?;
|
||||||
}
|
}
|
||||||
@@ -177,7 +181,7 @@ impl FileTransferActivity {
|
|||||||
error!("Could not leave alternate screen: {}", err);
|
error!("Could not leave alternate screen: {}", err);
|
||||||
}
|
}
|
||||||
// Lock ports
|
// Lock ports
|
||||||
assert!(self.app.lock_ports().is_ok());
|
ui_result(self.app.lock_ports());
|
||||||
// Get current file modification time
|
// Get current file modification time
|
||||||
let prev_mtime = self.get_localhost_mtime(path)?;
|
let prev_mtime = self.get_localhost_mtime(path)?;
|
||||||
// Open editor
|
// Open editor
|
||||||
@@ -205,7 +209,7 @@ impl FileTransferActivity {
|
|||||||
error!("Could not clear screen screen: {}", err);
|
error!("Could not clear screen screen: {}", err);
|
||||||
}
|
}
|
||||||
// Unlock ports
|
// Unlock ports
|
||||||
assert!(self.app.unlock_ports().is_ok());
|
ui_result(self.app.unlock_ports());
|
||||||
}
|
}
|
||||||
let after_mtime = self.get_localhost_mtime(path)?;
|
let after_mtime = self.get_localhost_mtime(path)?;
|
||||||
|
|
||||||
@@ -241,7 +245,8 @@ impl FileTransferActivity {
|
|||||||
return Err(format!("Could not open file {file_name}: {err}"));
|
return Err(format!("Could not open file {file_name}: {err}"));
|
||||||
}
|
}
|
||||||
// Get current file modification time
|
// Get current file modification time
|
||||||
let prev_mtime: SystemTime = match self.host_bridge.stat(tmpfile.as_path()) {
|
let prev_mtime: SystemTime = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path())
|
||||||
|
{
|
||||||
Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH),
|
Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@@ -254,7 +259,7 @@ impl FileTransferActivity {
|
|||||||
// Edit file
|
// Edit file
|
||||||
self.edit_local_file(tmpfile.as_path())?;
|
self.edit_local_file(tmpfile.as_path())?;
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry: File = match self.host_bridge.stat(tmpfile.as_path()) {
|
let tmpfile_entry: File = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@@ -280,7 +285,7 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry = match self.host_bridge.stat(tmpfile.as_path()) {
|
let tmpfile_entry = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -29,25 +29,22 @@ impl FromStr for Command {
|
|||||||
Err("cd command requires a path".to_string())
|
Err("cd command requires a path".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("exit") | Some("logout") => Ok(Command::Exit),
|
Some("exit" | "logout") => Ok(Command::Exit),
|
||||||
Some(cmd) => Ok(Command::Exec(cmd.to_string())),
|
Some(_) => Ok(Command::Exec(s.to_string())),
|
||||||
None => Err("".to_string()),
|
None => Err(String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_exec(&mut self, input: String) {
|
pub(crate) fn action_exec_cmd(&mut self, input: String) {
|
||||||
self.action_exec(false, input);
|
self.action_exec(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
fn action_exec(&mut self, cmd: String) {
|
||||||
self.action_exec(true, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn action_exec(&mut self, remote: bool, cmd: String) {
|
|
||||||
if cmd.is_empty() {
|
if cmd.is_empty() {
|
||||||
self.print_terminal("".to_string());
|
self.print_terminal(String::new());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmd = match Command::from_str(&cmd) {
|
let cmd = match Command::from_str(&cmd) {
|
||||||
@@ -61,10 +58,10 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
match cmd {
|
match cmd {
|
||||||
Command::Cd(path) => {
|
Command::Cd(path) => {
|
||||||
self.action_exec_cd(remote, path);
|
self.action_exec_cd(path);
|
||||||
}
|
}
|
||||||
Command::Exec(executable) => {
|
Command::Exec(executable) => {
|
||||||
self.action_exec_executable(remote, executable);
|
self.action_exec_executable(executable);
|
||||||
}
|
}
|
||||||
Command::Exit => {
|
Command::Exit => {
|
||||||
self.action_exec_exit();
|
self.action_exec_exit();
|
||||||
@@ -77,43 +74,20 @@ impl FileTransferActivity {
|
|||||||
self.umount_exec();
|
self.umount_exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action_exec_cd(&mut self, remote: bool, input: String) {
|
fn action_exec_cd(&mut self, input: String) {
|
||||||
let new_dir = if remote {
|
let dir_path = self.pane_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||||
let dir_path: PathBuf =
|
self.pane_changedir(dir_path.as_path(), true);
|
||||||
self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
|
||||||
self.remote_changedir(dir_path.as_path(), true);
|
|
||||||
|
|
||||||
dir_path
|
|
||||||
} else {
|
|
||||||
let dir_path: PathBuf =
|
|
||||||
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
|
||||||
self.host_bridge_changedir(dir_path.as_path(), true);
|
|
||||||
|
|
||||||
dir_path
|
|
||||||
};
|
|
||||||
|
|
||||||
self.update_browser_file_list();
|
self.update_browser_file_list();
|
||||||
|
|
||||||
// update prompt and print the new directory
|
// update prompt and print the new directory
|
||||||
self.update_terminal_prompt();
|
self.update_terminal_prompt();
|
||||||
self.print_terminal(new_dir.display().to_string());
|
self.print_terminal(dir_path.display().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a [`Command::Exec`] command
|
/// Execute a command via the active tab's pane.
|
||||||
fn action_exec_executable(&mut self, remote: bool, cmd: String) {
|
fn action_exec_executable(&mut self, cmd: String) {
|
||||||
let res = if remote {
|
match self.browser.fs_pane_mut().fs.exec(cmd.as_str()) {
|
||||||
self.client
|
|
||||||
.as_mut()
|
|
||||||
.exec(cmd.as_str())
|
|
||||||
.map(|(_, output)| output)
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
} else {
|
|
||||||
self.host_bridge
|
|
||||||
.exec(cmd.as_str())
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
self.print_terminal(output);
|
self.print_terminal(output);
|
||||||
}
|
}
|
||||||
@@ -122,7 +96,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Could not execute command \"{cmd}\": {err}"),
|
format!("Could not execute command \"{cmd}\": {err}"),
|
||||||
);
|
);
|
||||||
self.print_terminal(err);
|
self.print_terminal(err.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,14 @@
|
|||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
|
|
||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
enum Host {
|
|
||||||
HostBridge,
|
|
||||||
Remote,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
|
/// Calculate and display the total size of the selected file(s) via the active tab's pane.
|
||||||
pub(crate) fn action_get_file_size(&mut self) {
|
pub(crate) fn action_get_file_size(&mut self) {
|
||||||
// Get selected file
|
|
||||||
self.mount_blocking_wait("Getting total path size...");
|
self.mount_blocking_wait("Getting total path size...");
|
||||||
|
|
||||||
let total_size = match self.browser.tab() {
|
let files = self.get_selected_entries().get_files();
|
||||||
FileExplorerTab::HostBridge => {
|
let total_size = self.get_files_size(files);
|
||||||
let files = self.get_local_selected_entries().get_files();
|
|
||||||
self.get_files_size(files, Host::HostBridge)
|
|
||||||
}
|
|
||||||
FileExplorerTab::Remote => {
|
|
||||||
let files = self.get_remote_selected_entries().get_files();
|
|
||||||
self.get_files_size(files, Host::Remote)
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindHostBridge => {
|
|
||||||
let files = self.get_found_selected_entries().get_files();
|
|
||||||
self.get_files_size(files, Host::HostBridge)
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote => {
|
|
||||||
let files = self.get_found_selected_entries().get_files();
|
|
||||||
self.get_files_size(files, Host::Remote)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.umount_wait();
|
self.umount_wait();
|
||||||
self.mount_info(format!(
|
self.mount_info(format!(
|
||||||
@@ -40,24 +17,19 @@ impl FileTransferActivity {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_files_size(&mut self, files: Vec<File>, host: Host) -> u64 {
|
fn get_files_size(&mut self, files: Vec<File>) -> u64 {
|
||||||
files.into_iter().map(|f| self.get_file_size(f, host)).sum()
|
files.into_iter().map(|f| self.get_file_size(f)).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_file_size(&mut self, file: File, host: Host) -> u64 {
|
fn get_file_size(&mut self, file: File) -> u64 {
|
||||||
if let Some(symlink) = &file.metadata().symlink {
|
if let Some(symlink) = &file.metadata().symlink {
|
||||||
// stat
|
match self.browser.fs_pane_mut().fs.stat(symlink) {
|
||||||
let stat_res = match host {
|
|
||||||
Host::HostBridge => self.host_bridge.stat(symlink).map_err(|e| e.to_string()),
|
|
||||||
Host::Remote => self.client.stat(symlink).map_err(|e| e.to_string()),
|
|
||||||
};
|
|
||||||
match stat_res {
|
|
||||||
Ok(stat) => stat.metadata().size,
|
Ok(stat) => stat.metadata().size,
|
||||||
Err(err_msg) => {
|
Err(err) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Failed to stat symlink target {path}: {err_msg}",
|
"Failed to stat symlink target {path}: {err}",
|
||||||
path = symlink.display(),
|
path = symlink.display(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -65,22 +37,13 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if file.is_dir() {
|
} else if file.is_dir() {
|
||||||
// list and sum
|
match self.browser.fs_pane_mut().fs.list_dir(&file.path) {
|
||||||
let list_res = match host {
|
Ok(list) => list.into_iter().map(|f| self.get_file_size(f)).sum(),
|
||||||
Host::HostBridge => self
|
Err(err) => {
|
||||||
.host_bridge
|
|
||||||
.list_dir(&file.path)
|
|
||||||
.map_err(|e| e.to_string()),
|
|
||||||
Host::Remote => self.client.list_dir(&file.path).map_err(|e| e.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
match list_res {
|
|
||||||
Ok(list) => list.into_iter().map(|f| self.get_file_size(f, host)).sum(),
|
|
||||||
Err(err_msg) => {
|
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Failed to list directory {path}: {err_msg}",
|
"Failed to list directory {path}: {err}",
|
||||||
path = file.path.display(),
|
path = file.path.display(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::super::browser::FileExplorerTab;
|
use super::change_dir::SyncBrowsingDestination;
|
||||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -22,33 +22,27 @@ impl FileTransferActivity {
|
|||||||
Some(p) => p.to_path_buf(),
|
Some(p) => p.to_path_buf(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Change directory
|
// Change directory on the active tab's pane
|
||||||
match self.browser.tab() {
|
self.pane_changedir(path.as_path(), true);
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
// Sync browsing: mirror the directory change on the opposite pane
|
||||||
self.host_bridge_changedir(path.as_path(), true)
|
if self.browser.sync_browsing {
|
||||||
}
|
let dir_name = path
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
.file_name()
|
||||||
self.remote_changedir(path.as_path(), true)
|
.map(|n| n.to_string_lossy().to_string())
|
||||||
}
|
.unwrap_or_default();
|
||||||
|
self.synchronize_browsing(SyncBrowsingDestination::Path(dir_name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
|
pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
|
||||||
let wrkdir: PathBuf = match self.browser.tab() {
|
let wrkdir: PathBuf = self.browser.opposite_pane().explorer.wrkdir.clone();
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
|
||||||
self.remote().wrkdir.clone()
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
self.host_bridge().wrkdir.clone()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match self.get_found_selected_entries() {
|
match self.get_found_selected_entries() {
|
||||||
SelectedFile::One(entry) => match self.browser.tab() {
|
SelectedFile::One(entry) => {
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
if self.is_local_tab() {
|
||||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||||
if self.config().get_prompt_on_file_replace()
|
if self.config().get_prompt_on_file_replace()
|
||||||
&& self.remote_file_exists(file_to_check.as_path())
|
&& self.file_exists(file_to_check.as_path(), false)
|
||||||
&& !self.should_replace_file(
|
&& !self.should_replace_file(
|
||||||
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
||||||
)
|
)
|
||||||
@@ -66,11 +60,10 @@ impl FileTransferActivity {
|
|||||||
format!("Could not upload file: {err}"),
|
format!("Could not upload file: {err}"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||||
if self.config().get_prompt_on_file_replace()
|
if self.config().get_prompt_on_file_replace()
|
||||||
&& self.host_bridge_file_exists(file_to_check.as_path())
|
&& self.file_exists(file_to_check.as_path(), true)
|
||||||
&& !self.should_replace_file(
|
&& !self.should_replace_file(
|
||||||
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
||||||
)
|
)
|
||||||
@@ -89,7 +82,7 @@ impl FileTransferActivity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// In case of selection: save multiple files in wrkdir/input
|
// In case of selection: save multiple files in wrkdir/input
|
||||||
let mut dest_path: PathBuf = wrkdir;
|
let mut dest_path: PathBuf = wrkdir;
|
||||||
@@ -97,58 +90,51 @@ impl FileTransferActivity {
|
|||||||
dest_path.push(save_as);
|
dest_path.push(save_as);
|
||||||
}
|
}
|
||||||
// Iter files
|
// Iter files
|
||||||
match self.browser.tab() {
|
if self.is_local_tab() {
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
|
||||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
self.get_files_to_transfer_with_overwrites(
|
||||||
entries,
|
|
||||||
) = self.get_files_to_transfer_with_overwrites(
|
|
||||||
entries,
|
entries,
|
||||||
super::save::CheckFileExists::Remote,
|
super::save::CheckFileExists::Remote,
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
debug!("User cancelled file transfer due to overwrites");
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Err(err) = self.filetransfer_send(
|
if let Err(err) = self.filetransfer_send(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
{
|
self.log_and_alert(
|
||||||
self.log_and_alert(
|
LogLevel::Error,
|
||||||
LogLevel::Error,
|
format!("Could not upload file: {err}"),
|
||||||
format!("Could not upload file: {err}"),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
} else {
|
||||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
|
||||||
entries,
|
self.get_files_to_transfer_with_overwrites(
|
||||||
) = self.get_files_to_transfer_with_overwrites(
|
|
||||||
entries,
|
entries,
|
||||||
super::save::CheckFileExists::HostBridge,
|
super::save::CheckFileExists::HostBridge,
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
debug!("User cancelled file transfer due to overwrites");
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Err(err) = self.filetransfer_recv(
|
if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Could not download file: {err}"),
|
format!("Could not download file: {err}"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear selection
|
// clear selection
|
||||||
if let Some(f) = self.found_mut() {
|
if let Some(f) = self.found_mut() {
|
||||||
f.clear_queue();
|
f.clear_queue();
|
||||||
self.update_find_list();
|
self.update_find_list();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,14 +145,11 @@ impl FileTransferActivity {
|
|||||||
pub(crate) fn action_find_delete(&mut self) {
|
pub(crate) fn action_find_delete(&mut self) {
|
||||||
match self.get_found_selected_entries() {
|
match self.get_found_selected_entries() {
|
||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
// Delete file
|
self.remove_file(&entry);
|
||||||
self.remove_found_file(&entry);
|
|
||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
|
||||||
for (entry, _) in entries.iter() {
|
for (entry, _) in entries.iter() {
|
||||||
// Delete file
|
self.remove_file(entry);
|
||||||
self.remove_found_file(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear selection
|
// clear selection
|
||||||
@@ -179,17 +162,6 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_found_file(&mut self, entry: &File) {
|
|
||||||
match self.browser.tab() {
|
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
|
||||||
self.local_remove_file(entry);
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
self.remote_remove_file(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn action_find_open(&mut self) {
|
pub(crate) fn action_find_open(&mut self) {
|
||||||
match self.get_found_selected_entries() {
|
match self.get_found_selected_entries() {
|
||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
@@ -235,13 +207,6 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn open_found_file(&mut self, entry: &File, with: Option<&str>) {
|
fn open_found_file(&mut self, entry: &File, with: Option<&str>) {
|
||||||
match self.browser.tab() {
|
self.open_file(entry, with);
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
|
||||||
self.action_open_local_file(entry, with);
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
self.action_open_remote_file(entry, with);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use super::FileTransferActivity;
|
use super::FileTransferActivity;
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,46 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use remotefs::fs::UnixPex;
|
|
||||||
|
|
||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_mkdir(&mut self, input: String) {
|
/// Create a directory via the active tab's pane.
|
||||||
match self
|
pub(crate) fn action_mkdir(&mut self, input: String) {
|
||||||
.host_bridge
|
let path = PathBuf::from(input.as_str());
|
||||||
.mkdir(PathBuf::from(input.as_str()).as_path())
|
match self.browser.fs_pane_mut().fs.mkdir(path.as_path()) {
|
||||||
{
|
Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")),
|
||||||
Ok(_) => {
|
Err(err) => self.log_and_alert(
|
||||||
// Reload files
|
LogLevel::Error,
|
||||||
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
format!("Could not create directory \"{input}\": {err}"),
|
||||||
}
|
),
|
||||||
Err(err) => {
|
|
||||||
// Report err
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not create directory \"{input}\": {err}"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub(crate) fn action_remote_mkdir(&mut self, input: String) {
|
|
||||||
match self.client.as_mut().create_dir(
|
/// Create a directory on the local host (used by sync-browsing in change_dir).
|
||||||
PathBuf::from(input.as_str()).as_path(),
|
pub(in crate::ui::activities::filetransfer) fn action_local_mkdir(&mut self, input: String) {
|
||||||
UnixPex::from(0o755),
|
let path = PathBuf::from(input.as_str());
|
||||||
) {
|
match self.browser.local_pane_mut().fs.mkdir(path.as_path()) {
|
||||||
Ok(_) => {
|
Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")),
|
||||||
// Reload files
|
Err(err) => self.log_and_alert(
|
||||||
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
LogLevel::Error,
|
||||||
}
|
format!("Could not create directory \"{input}\": {err}"),
|
||||||
Err(err) => {
|
),
|
||||||
// Report err
|
}
|
||||||
self.log_and_alert(
|
}
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not create directory \"{input}\": {err}"),
|
/// Create a directory on the remote host (used by sync-browsing in change_dir).
|
||||||
);
|
pub(in crate::ui::activities::filetransfer) fn action_remote_mkdir(&mut self, input: String) {
|
||||||
}
|
let path = PathBuf::from(input.as_str());
|
||||||
|
match self.browser.remote_pane_mut().fs.mkdir(path.as_path()) {
|
||||||
|
Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")),
|
||||||
|
Err(err) => self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not create directory \"{input}\": {err}"),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user