mirror of
https://github.com/veeso/termscp.git
synced 2026-09-19 10:35:52 -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:
|
||||
|
||||
env:
|
||||
TERMSCP_VERSION: "0.19.0"
|
||||
TERMSCP_VERSION: "1.0.0"
|
||||
|
||||
jobs:
|
||||
build-binaries:
|
||||
@@ -28,14 +28,25 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
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
|
||||
os: windows-latest
|
||||
platform: windows
|
||||
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 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
@@ -148,7 +159,7 @@ jobs:
|
||||
with:
|
||||
if-no-files-found: error
|
||||
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
|
||||
|
||||
- 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:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt install -y curl wget libsmbclient
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install termscp from script
|
||||
run: |
|
||||
./install.sh -v=0.12.3 -f
|
||||
./install.sh -f
|
||||
which termscp || exit 1
|
||||
termscp --version
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
@@ -14,13 +15,19 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install dependencies
|
||||
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
|
||||
with:
|
||||
toolchain: stable
|
||||
@@ -30,7 +37,5 @@ jobs:
|
||||
with:
|
||||
command: test
|
||||
args: --no-default-features --features github-actions --no-fail-fast
|
||||
- name: Format
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Clippy
|
||||
run: cargo clippy -- -Dwarnings
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
@@ -14,10 +15,10 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
@@ -14,11 +15,11 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
@@ -24,3 +24,7 @@ dist/pkgs/arch/*.tar.gz
|
||||
|
||||
dist/pkgs/
|
||||
dist/build/macos/openssl/
|
||||
|
||||
.idea/
|
||||
.claude/
|
||||
|
||||
|
||||
+218
-40
@@ -1,47 +1,225 @@
|
||||
# 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
|
||||
|
||||
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:
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Generated
+2455
-960
File diff suppressed because it is too large
Load Diff
+106
-91
@@ -1,17 +1,17 @@
|
||||
[package]
|
||||
authors = ["Christian Visintin <christian.visintin@veeso.dev>"]
|
||||
categories = ["command-line-utilities"]
|
||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV"
|
||||
name = "termscp"
|
||||
version = "1.0.0"
|
||||
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"
|
||||
include = ["src/**/*", "build.rs", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
|
||||
license = "MIT"
|
||||
name = "termscp"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
version = "0.19.0"
|
||||
rust-version = "1.88.0"
|
||||
rust-version = "1.89.0"
|
||||
|
||||
[package.metadata.rpm]
|
||||
package = "termscp"
|
||||
@@ -27,96 +27,111 @@ maintainer = "Christian Visintin <christian.visintin@veeso.dev>"
|
||||
copyright = "2025, Christian Visintin <christian.visintin@veeso.dev>"
|
||||
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]
|
||||
default = ["smb", "keyring"]
|
||||
default = ["keyring", "smb"]
|
||||
github-actions = []
|
||||
isolated-tests = []
|
||||
keyring = []
|
||||
smb = ["dep:remotefs-smb"]
|
||||
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]
|
||||
incremental = true
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</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">
|
||||
<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\
|
||||
prefix: \"/usr/local/bin\"\n\
|
||||
deps: {\n\
|
||||
libssh: {origin: security/libssh, version: 0.9.5}\n\
|
||||
}\n\
|
||||
files: {\n\
|
||||
/usr/local/bin/termscp: \"$HASH\"\n\
|
||||
|
||||
+2
-4
@@ -71,7 +71,7 @@
|
||||
</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">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -193,12 +193,10 @@ Für weitere Informationen oder andere Plattformen besuchen Sie bitte [termscp.v
|
||||
### Softwareanforderungen ❗
|
||||
|
||||
- **Linux** Benutzer:
|
||||
- libssh
|
||||
- libdbus-1
|
||||
- pkg-config
|
||||
- libsmbclient
|
||||
- **FreeBSD** Benutzer:
|
||||
- libssh
|
||||
- dbus
|
||||
- pkgconf
|
||||
- libsmbclient
|
||||
@@ -265,7 +263,7 @@ termscp wird von diesen großartigen Projekten unterstützt:
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [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)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@
|
||||
</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">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -263,7 +263,7 @@ termscp funciona con estos increíbles proyectos:
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [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)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@
|
||||
</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">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -265,7 +265,7 @@ termscp est soutenu par ces projets impressionnants:
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [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)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@
|
||||
</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">
|
||||
<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)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [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)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</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">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</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">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -196,12 +196,10 @@ choco install termscp
|
||||
### 依赖 ❗
|
||||
|
||||
- **Linux** 用户:
|
||||
- libssh
|
||||
- libdbus-1
|
||||
- pkg-config
|
||||
- libsmbclient
|
||||
- **FreeBSD** 用户:
|
||||
- libssh
|
||||
- dbus
|
||||
- pkgconf
|
||||
- libsmbclient
|
||||
@@ -270,7 +268,7 @@ termscp 由这些很棒的项目提供支持:
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [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)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
|
||||
+52
-30
@@ -8,10 +8,10 @@
|
||||
# -f, -y, --force, --yes
|
||||
# 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}"
|
||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_amd64.deb"
|
||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_arm64.deb"
|
||||
|
||||
PATH="$PATH:/usr/sbin"
|
||||
|
||||
@@ -33,8 +33,8 @@ NO_COLOR="$(tput sgr0 2>/dev/null || printf '')"
|
||||
set_termscp_version() {
|
||||
TERMSCP_VERSION="$1"
|
||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_amd64.deb"
|
||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_arm64.deb"
|
||||
}
|
||||
|
||||
info() {
|
||||
@@ -217,6 +217,49 @@ install_with_brew() {
|
||||
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() {
|
||||
local msg
|
||||
local sudo
|
||||
@@ -236,28 +279,7 @@ install_on_linux() {
|
||||
elif has pikaur; then
|
||||
install_on_arch_linux pikaur
|
||||
elif has dpkg; then
|
||||
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 "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}
|
||||
install_on_debian
|
||||
elif has brew; then
|
||||
install_with_brew
|
||||
else
|
||||
@@ -277,9 +299,9 @@ install_on_macos() {
|
||||
|
||||
install_bsd_cargo_deps() {
|
||||
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 pkg install -y curl wget libssh gcc dbus pkgconf libsmbclient
|
||||
$sudo pkg install -y curl wget gcc dbus pkgconf libsmbclient
|
||||
info "Dependencies installed successfully"
|
||||
}
|
||||
|
||||
@@ -305,7 +327,7 @@ install_linux_cargo_deps() {
|
||||
exit 1
|
||||
fi
|
||||
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 $deps_cmd
|
||||
info "Dependencies installed successfully"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
||||
since last release, so if the latest version is not available yet,
|
||||
you can install it downloading the ZIP file from</span>
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.19.0.nupkg"
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.1.0.0.nupkg"
|
||||
target="_blank">Github</a>
|
||||
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
||||
</p>
|
||||
@@ -74,7 +74,7 @@
|
||||
On Debian based distros, you can install termscp using the Deb
|
||||
package via:
|
||||
</p>
|
||||
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.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>
|
||||
</div>
|
||||
<h3>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
</button>
|
||||
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
||||
<p class="text-lg">
|
||||
<span translate="intro.versionAlert">termscp 0.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>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Get started →",
|
||||
"versionAlert": "termscp 0.19.0 is NOW out! Download it from",
|
||||
"versionAlert": "termscp 1.0.0 is NOW out! Download it from",
|
||||
"here": "here",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Para iniciar →",
|
||||
"versionAlert": "termscp 0.19.0 ya está disponible! Descárgalo desde",
|
||||
"versionAlert": "termscp 1.0.0 ya está disponible! Descárgalo desde",
|
||||
"here": "aquì",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Pour commencer →",
|
||||
"versionAlert": "termscp 0.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",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
||||
"getStarted": "Installa termscp →",
|
||||
"versionAlert": "termscp 0.19.0 è ORA disponbile! Scaricalo da",
|
||||
"versionAlert": "termscp 1.0.0 è ORA disponbile! Scaricalo da",
|
||||
"here": "qui",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "开始 →",
|
||||
"versionAlert": "termscp 0.19.0 现已发布! 从下载",
|
||||
"versionAlert": "termscp 1.0.0 现已发布! 从下载",
|
||||
"here": "这里",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
+37
-21
@@ -94,7 +94,9 @@ impl ActivityManager {
|
||||
// local dir is remote_args.local_dir if set, otherwise current dir
|
||||
let local_dir = remote_args
|
||||
.local_dir
|
||||
.unwrap_or_else(|| env::current_dir().unwrap());
|
||||
.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,);
|
||||
|
||||
self.set_host_params(
|
||||
@@ -143,27 +145,26 @@ impl ActivityManager {
|
||||
|
||||
match host {
|
||||
HostParams::HostBridge(HostBridgeParams::Localhost(path)) => {
|
||||
self.context
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
self.context_mut()?
|
||||
.set_host_bridge_params(HostBridgeParams::Localhost(path));
|
||||
}
|
||||
HostParams::HostBridge(HostBridgeParams::Remote(_, _)) => {
|
||||
let (protocol, params) = remote_params.unwrap();
|
||||
self.context
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
let (protocol, params) = remote_params.ok_or_else(|| {
|
||||
String::from("Missing remote parameters for host bridge configuration")
|
||||
})?;
|
||||
self.context_mut()?
|
||||
.set_host_bridge_params(HostBridgeParams::Remote(protocol, params));
|
||||
}
|
||||
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 {
|
||||
local_path: remote_local_path,
|
||||
remote_path: remote_remote_path,
|
||||
protocol,
|
||||
params,
|
||||
};
|
||||
self.context.as_mut().unwrap().set_remote_params(params);
|
||||
self.context_mut()?.set_remote_params(params);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -185,16 +186,19 @@ impl ActivityManager {
|
||||
) && 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
|
||||
let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config());
|
||||
let generic_params = params.generic_params().unwrap();
|
||||
let storage = SshKeyStorage::from(self.context_ref()?.config());
|
||||
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
|
||||
.resolve(
|
||||
&generic_params.address,
|
||||
&generic_params
|
||||
.username
|
||||
.clone()
|
||||
.unwrap_or(whoami::username()),
|
||||
)
|
||||
.resolve(&generic_params.address, &username)
|
||||
.is_none()
|
||||
{
|
||||
debug!(
|
||||
@@ -218,7 +222,7 @@ impl ActivityManager {
|
||||
|
||||
/// Prompt user for password to set into params.
|
||||
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());
|
||||
|
||||
match tty::read_secret_from_tty(ctx.terminal(), prompt) {
|
||||
@@ -243,7 +247,7 @@ impl ActivityManager {
|
||||
bookmark_name: &str,
|
||||
password: Option<&str>,
|
||||
) -> 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) {
|
||||
None => {
|
||||
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
|
||||
/// 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 super::Args;
|
||||
@@ -13,8 +18,11 @@ enum AddrType {
|
||||
/// Args for remote connection
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteArgs {
|
||||
/// Optional host bridge selected for the session.
|
||||
pub host_bridge: Remote,
|
||||
/// Target remote selected for the session.
|
||||
pub remote: Remote,
|
||||
/// Optional local working directory override.
|
||||
pub local_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -41,9 +49,7 @@ impl TryFrom<&Args> for RemoteArgs {
|
||||
(_, _) => Err("Too many arguments".to_string()),
|
||||
}?;
|
||||
// parse bookmark first
|
||||
let last_item_index = (args.bookmark.len() + args.positional.len())
|
||||
.checked_sub(1)
|
||||
.unwrap_or_default();
|
||||
let last_item_index = (args.bookmark.len() + args.positional.len()).saturating_sub(1);
|
||||
|
||||
let mut hosts = vec![];
|
||||
|
||||
@@ -75,10 +81,16 @@ impl TryFrom<&Args> for RemoteArgs {
|
||||
|
||||
// set args based on hosts len
|
||||
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 {
|
||||
remote_args.host_bridge = hosts.pop().unwrap();
|
||||
remote_args.remote = hosts.pop().unwrap();
|
||||
remote_args.host_bridge = hosts
|
||||
.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)
|
||||
@@ -105,6 +117,7 @@ pub enum Remote {
|
||||
}
|
||||
|
||||
impl Remote {
|
||||
/// Returns whether this CLI slot was left unspecified.
|
||||
pub fn is_none(&self) -> bool {
|
||||
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 crate::filetransfer::params::AwsS3Params;
|
||||
@@ -5,13 +9,20 @@ use crate::filetransfer::params::AwsS3Params;
|
||||
/// Connection parameters for Aws s3 protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct S3Params {
|
||||
/// Bucket name to open.
|
||||
pub bucket: String,
|
||||
/// AWS region used for the bucket.
|
||||
pub region: Option<String>,
|
||||
/// Custom endpoint URL for S3-compatible services.
|
||||
pub endpoint: Option<String>,
|
||||
/// Shared credentials profile name.
|
||||
pub profile: Option<String>,
|
||||
/// Static access key identifier.
|
||||
pub access_key: Option<String>,
|
||||
/// Static secret access key.
|
||||
pub secret_access_key: Option<String>,
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! ## Bookmark Kube Parameters
|
||||
//!
|
||||
//! Stores bookmark-specific Kubernetes connection settings.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::filetransfer::params::KubeProtocolParams;
|
||||
@@ -5,10 +9,15 @@ use crate::filetransfer::params::KubeProtocolParams;
|
||||
/// Extra Connection parameters for Kube protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct KubeParams {
|
||||
/// Optional default namespace.
|
||||
pub namespace: Option<String>,
|
||||
/// Optional cluster API URL.
|
||||
pub cluster_url: Option<String>,
|
||||
/// Optional Kubernetes username override.
|
||||
pub username: Option<String>,
|
||||
/// Optional client certificate path.
|
||||
pub client_cert: Option<String>,
|
||||
/// Optional client key path.
|
||||
pub client_key: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! ## Bookmark SMB Parameters
|
||||
//!
|
||||
//! Stores bookmark-specific SMB share configuration.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::filetransfer::params::SmbParams as TransferSmbParams;
|
||||
@@ -5,7 +9,9 @@ use crate::filetransfer::params::SmbParams as TransferSmbParams;
|
||||
/// Extra Connection parameters for SMB protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct SmbParams {
|
||||
/// SMB share name.
|
||||
pub share: String,
|
||||
/// Optional SMB workgroup used on POSIX platforms.
|
||||
pub workgroup: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -442,6 +442,34 @@ mod tests {
|
||||
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]
|
||||
fn test_config_serializer_bookmarks_serializer_serialize() {
|
||||
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().rewind().unwrap();
|
||||
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();
|
||||
toml_file.as_file().sync_all().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]
|
||||
@@ -693,6 +723,39 @@ mod tests {
|
||||
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 {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r##"auth_address = "Yellow"
|
||||
@@ -714,8 +777,7 @@ mod tests {
|
||||
transfer_local_explorer_highlighted = "Yellow"
|
||||
transfer_log_background = "255, 255, 255"
|
||||
transfer_log_window = "LightGreen"
|
||||
transfer_progress_bar_full = "forestgreen"
|
||||
transfer_progress_bar_partial = "Green"
|
||||
transfer_progress_bar = "forestgreen"
|
||||
transfer_remote_explorer_background = "#f0f0f0"
|
||||
transfer_remote_explorer_foreground = "rgb(40, 40, 40)"
|
||||
transfer_remote_explorer_highlighted = "LightBlue"
|
||||
|
||||
+208
-126
@@ -12,145 +12,62 @@ use crate::utils::fmt::fmt_color;
|
||||
use crate::utils::parser::parse_color;
|
||||
|
||||
/// 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 {
|
||||
// -- auth
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_address: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_bookmarks: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_password: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_port: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_protocol: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_recents: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_username: Color,
|
||||
// -- misc
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_error_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_info_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_input_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_keys: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_quit_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_save_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_warn_dialog: Color,
|
||||
// -- transfer
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_local_explorer_background: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_local_explorer_foreground: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_local_explorer_highlighted: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_log_background: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_log_window: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
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"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_progress_bar: Color,
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_remote_explorer_background: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_remote_explorer_foreground: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_remote_explorer_highlighted: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_status_hidden: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_status_sorting: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_status_sync_browsing: Color,
|
||||
}
|
||||
|
||||
@@ -176,8 +93,7 @@ impl Default for Theme {
|
||||
transfer_local_explorer_highlighted: Color::Yellow,
|
||||
transfer_log_background: Color::Reset,
|
||||
transfer_log_window: Color::LightGreen,
|
||||
transfer_progress_bar_partial: Color::Green,
|
||||
transfer_progress_bar_full: Color::Green,
|
||||
transfer_progress_bar: Color::Green,
|
||||
transfer_remote_explorer_background: Color::Reset,
|
||||
transfer_remote_explorer_foreground: Color::Reset,
|
||||
transfer_remote_explorer_highlighted: Color::LightBlue,
|
||||
@@ -189,16 +105,124 @@ impl Default for Theme {
|
||||
}
|
||||
|
||||
// -- 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>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: String = Deserialize::deserialize(deserializer)?;
|
||||
// Parse color
|
||||
match parse_color(&s) {
|
||||
None => Err(DeError::custom("Invalid color")),
|
||||
Some(color) => Ok(color),
|
||||
#[derive(Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
struct ThemeFile {
|
||||
auth_address: Option<String>,
|
||||
auth_bookmarks: Option<String>,
|
||||
auth_password: Option<String>,
|
||||
auth_port: Option<String>,
|
||||
auth_protocol: Option<String>,
|
||||
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())
|
||||
}
|
||||
|
||||
// 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)]
|
||||
mod test {
|
||||
mod tests {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_themes_default() {
|
||||
fn should_get_default_theme() {
|
||||
let theme: Theme = Theme::default();
|
||||
assert_eq!(theme.auth_address, Color::Yellow);
|
||||
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_log_background, Color::Reset);
|
||||
assert_eq!(theme.transfer_log_window, Color::LightGreen);
|
||||
assert_eq!(theme.transfer_progress_bar_full, Color::Green);
|
||||
assert_eq!(theme.transfer_progress_bar_partial, Color::Green);
|
||||
assert_eq!(theme.transfer_progress_bar, Color::Green);
|
||||
assert_eq!(theme.transfer_remote_explorer_background, Color::Reset);
|
||||
assert_eq!(theme.transfer_remote_explorer_foreground, Color::Reset);
|
||||
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_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
|
||||
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
|
||||
@@ -24,7 +24,7 @@ impl FileExplorerBuilder {
|
||||
|
||||
/// Take FileExplorer out of builder
|
||||
pub fn build(&mut self) -> FileExplorer {
|
||||
self.explorer.take().unwrap()
|
||||
self.explorer.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Enable HIDDEN_FILES option
|
||||
|
||||
+41
-21
@@ -108,7 +108,7 @@ impl CallChainBlock {
|
||||
None => {
|
||||
self.next_block = Some(Box::new(CallChainBlock::new(
|
||||
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
|
||||
//format!("{cur_str}{prefix}{size:10}", size = size.display().si())
|
||||
} else if fsentry.metadata().symlink.is_some() {
|
||||
let size = ByteSize(
|
||||
fsentry
|
||||
.metadata()
|
||||
.symlink
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.len() as u64,
|
||||
);
|
||||
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(' ');
|
||||
}
|
||||
match fsentry.metadata().symlink.as_ref() {
|
||||
Some(symlink) => {
|
||||
let size = ByteSize(symlink.to_string_lossy().len() as u64);
|
||||
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 {
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{cur_str}{prefix} ")
|
||||
@@ -476,12 +473,14 @@ impl Formatter {
|
||||
let mut last_index: usize = 0;
|
||||
// Match fmt str against regex
|
||||
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
|
||||
// Get match index (unwrap is safe, since always exists)
|
||||
let index: usize = fmt_str.find(®ex_match[0]).unwrap();
|
||||
let Some(full_match) = regex_match.get(0) else {
|
||||
continue;
|
||||
};
|
||||
let index: usize = full_match.start();
|
||||
// Get prefix
|
||||
let prefix: String = String::from(&fmt_str[last_index..index]);
|
||||
// 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 FMT_ATTR_REGEX.captures(®ex_match[1]) {
|
||||
Some(regex_match) => {
|
||||
@@ -516,7 +515,7 @@ impl Formatter {
|
||||
match callchain.as_mut() {
|
||||
None => {
|
||||
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),
|
||||
}
|
||||
@@ -1027,6 +1026,27 @@ mod tests {
|
||||
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
|
||||
fn dummy_fmt(
|
||||
_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 crate::host::{HostBridge, Localhost, RemoteBridged};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
|
||||
/// Builds the host-side filesystem bridge used during file transfer sessions.
|
||||
pub struct 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(
|
||||
params: HostBridgeParams,
|
||||
config_client: &ConfigClient,
|
||||
|
||||
@@ -25,17 +25,17 @@ pub enum HostBridgeParams {
|
||||
}
|
||||
|
||||
impl HostBridgeParams {
|
||||
pub fn unwrap_protocol_params(&self) -> &ProtocolParams {
|
||||
pub fn protocol_params(&self) -> Option<&ProtocolParams> {
|
||||
match self {
|
||||
HostBridgeParams::Localhost(_) => panic!("Localhost has no protocol params"),
|
||||
HostBridgeParams::Remote(_, params) => params,
|
||||
HostBridgeParams::Localhost(_) => None,
|
||||
HostBridgeParams::Remote(_, params) => Some(params),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the host name for the bridge params
|
||||
pub fn username(&self) -> Option<String> {
|
||||
match self {
|
||||
HostBridgeParams::Localhost(_) => Some(whoami::username()),
|
||||
HostBridgeParams::Localhost(_) => whoami::username().ok(),
|
||||
HostBridgeParams::Remote(_, params) => {
|
||||
params.generic_params().and_then(|p| p.username.clone())
|
||||
}
|
||||
@@ -278,6 +278,19 @@ mod test {
|
||||
|
||||
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]
|
||||
fn test_filetransfer_params() {
|
||||
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
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AwsS3Params {
|
||||
/// Target bucket name.
|
||||
pub bucket_name: String,
|
||||
/// Target region.
|
||||
pub region: Option<String>,
|
||||
/// Optional custom endpoint URL.
|
||||
pub endpoint: Option<String>,
|
||||
/// Optional shared credentials profile.
|
||||
pub profile: Option<String>,
|
||||
/// Optional static access key.
|
||||
pub access_key: Option<String>,
|
||||
/// Optional static secret access key.
|
||||
pub secret_access_key: Option<String>,
|
||||
/// Optional security token for temporary credentials.
|
||||
pub security_token: Option<String>,
|
||||
/// Optional session token for temporary credentials.
|
||||
pub session_token: Option<String>,
|
||||
/// Whether to force path-style bucket addressing.
|
||||
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;
|
||||
|
||||
/// Protocol params used by WebDAV
|
||||
/// Protocol params used by Kubernetes connections.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KubeProtocolParams {
|
||||
/// Optional namespace for the default pod context.
|
||||
pub namespace: Option<String>,
|
||||
/// Optional Kubernetes API URL.
|
||||
pub cluster_url: Option<String>,
|
||||
/// Optional username override.
|
||||
pub username: Option<String>,
|
||||
/// Optional client certificate path.
|
||||
pub client_cert: Option<String>,
|
||||
/// Optional client key path.
|
||||
pub client_key: Option<String>,
|
||||
}
|
||||
|
||||
impl KubeProtocolParams {
|
||||
/// Kubernetes connections do not use the shared password secret flow.
|
||||
pub fn set_default_secret(&mut self, _secret: String) {}
|
||||
|
||||
/// Kubernetes params never require the generic password prompt.
|
||||
pub fn password_missing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Converts bookmark/runtime parameters into a `remotefs_kube` config.
|
||||
pub fn config(self) -> Option<Config> {
|
||||
if let Some(cluster_url) = self.cluster_url {
|
||||
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
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmbParams {
|
||||
/// Hostname or address of the SMB server.
|
||||
pub address: String,
|
||||
#[cfg(posix)]
|
||||
/// SMB service port used on POSIX platforms.
|
||||
pub port: u16,
|
||||
/// Share name to mount.
|
||||
pub share: String,
|
||||
/// Optional username.
|
||||
pub username: Option<String>,
|
||||
/// Optional password.
|
||||
pub password: Option<String>,
|
||||
#[cfg(posix)]
|
||||
/// Optional workgroup used on POSIX platforms.
|
||||
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
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebDAVProtocolParams {
|
||||
/// Base WebDAV endpoint URI.
|
||||
pub uri: String,
|
||||
/// Username used for authentication.
|
||||
pub username: String,
|
||||
/// Password used for authentication.
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl WebDAVProtocolParams {
|
||||
/// Stores the shared secret as the active WebDAV password.
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
self.password = secret;
|
||||
}
|
||||
|
||||
/// Returns whether the WebDAV password is currently missing.
|
||||
pub fn password_missing(&self) -> bool {
|
||||
self.password.is_empty()
|
||||
}
|
||||
|
||||
@@ -13,11 +13,10 @@ use remotefs_kube::KubeMultiPodFs as KubeFs;
|
||||
use remotefs_smb::SmbOptions;
|
||||
#[cfg(smb)]
|
||||
use remotefs_smb::{SmbCredentials, SmbFs};
|
||||
#[cfg(windows)]
|
||||
use remotefs_ssh::LibSsh2Session as SshSession;
|
||||
#[cfg(unix)]
|
||||
use remotefs_ssh::LibSshSession as SshSession;
|
||||
use remotefs_ssh::{ScpFs, SftpFs, SshAgentIdentity, SshConfigParseRule, SshOpts};
|
||||
use remotefs_ssh::{
|
||||
NoCheckServerKey, RusshSession as SshSession, ScpFs, SftpFs, SshAgentIdentity,
|
||||
SshConfigParseRule, SshOpts,
|
||||
};
|
||||
use remotefs_webdav::WebDAVFs;
|
||||
|
||||
#[cfg(not(smb))]
|
||||
@@ -44,23 +43,23 @@ impl RemoteFsBuilder {
|
||||
) -> Result<Box<dyn RemoteFs>, String> {
|
||||
match (protocol, 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)) => {
|
||||
Ok(Box::new(Self::ftp_client(params, secure)))
|
||||
}
|
||||
(FileTransferProtocol::Kube, ProtocolParams::Kube(params)) => {
|
||||
Ok(Box::new(Self::kube_client(params)))
|
||||
Ok(Box::new(Self::kube_client(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)) => {
|
||||
Ok(Box::new(Self::sftp_client(params, config_client)))
|
||||
Ok(Box::new(Self::sftp_client(params, config_client)?))
|
||||
}
|
||||
#[cfg(smb)]
|
||||
(FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => {
|
||||
Ok(Box::new(Self::smb_client(params)))
|
||||
Ok(Box::new(Self::smb_client(params)?))
|
||||
}
|
||||
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
|
||||
Ok(Box::new(Self::webdav_client(params)))
|
||||
@@ -75,13 +74,13 @@ impl RemoteFsBuilder {
|
||||
}
|
||||
|
||||
/// 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(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Unable to create tokio runtime"),
|
||||
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
|
||||
);
|
||||
let mut client =
|
||||
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 {
|
||||
client = client.session_token(session_token);
|
||||
}
|
||||
client
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Build ftp client from parameters
|
||||
@@ -125,19 +124,13 @@ impl RemoteFsBuilder {
|
||||
}
|
||||
|
||||
/// Build kube client
|
||||
fn kube_client(params: KubeProtocolParams) -> KubeFs {
|
||||
let rt = Arc::new(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Unable to create tokio runtime"),
|
||||
);
|
||||
fn kube_client(params: KubeProtocolParams) -> Result<KubeFs, String> {
|
||||
let rt = Self::tokio_runtime()?;
|
||||
let kube_fs = KubeFs::new(&rt);
|
||||
if let Some(config) = params.config() {
|
||||
kube_fs.config(config)
|
||||
Ok(kube_fs.config(config))
|
||||
} else {
|
||||
kube_fs
|
||||
Ok(kube_fs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,20 +138,24 @@ impl RemoteFsBuilder {
|
||||
fn scp_client(
|
||||
params: GenericProtocolParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> ScpFs<SshSession> {
|
||||
Self::build_ssh_opts(params, config_client).into()
|
||||
) -> Result<ScpFs<SshSession<NoCheckServerKey>>, String> {
|
||||
let opts = Self::build_ssh_opts(params, config_client);
|
||||
let rt = Self::tokio_runtime()?;
|
||||
Ok(ScpFs::russh(opts, rt))
|
||||
}
|
||||
|
||||
/// Build sftp client
|
||||
fn sftp_client(
|
||||
params: GenericProtocolParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> SftpFs<SshSession> {
|
||||
Self::build_ssh_opts(params, config_client).into()
|
||||
) -> Result<SftpFs<SshSession<NoCheckServerKey>>, String> {
|
||||
let opts = Self::build_ssh_opts(params, config_client);
|
||||
let rt = Self::tokio_runtime()?;
|
||||
Ok(SftpFs::russh(opts, rt))
|
||||
}
|
||||
|
||||
#[cfg(smb_unix)]
|
||||
fn smb_client(params: SmbParams) -> SmbFs {
|
||||
fn smb_client(params: SmbParams) -> Result<SmbFs, String> {
|
||||
let mut credentials = SmbCredentials::default()
|
||||
.server(format!("smb://{}:{}", params.address, params.port))
|
||||
.share(params.share);
|
||||
@@ -173,22 +170,20 @@ impl RemoteFsBuilder {
|
||||
credentials = credentials.workgroup(workgroup);
|
||||
}
|
||||
|
||||
match SmbFs::try_new(
|
||||
SmbFs::try_new(
|
||||
credentials,
|
||||
SmbOptions::default()
|
||||
.one_share_per_server(true)
|
||||
.case_sensitive(false),
|
||||
) {
|
||||
Ok(fs) => fs,
|
||||
Err(e) => {
|
||||
error!("Invalid params for protocol SMB: {e}");
|
||||
panic!("Invalid params for protocol SMB: {e}")
|
||||
}
|
||||
}
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("Invalid params for protocol SMB: {e}");
|
||||
format!("Invalid params for protocol SMB: {e}")
|
||||
})
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
||||
if let Some(username) = params.username {
|
||||
@@ -198,7 +193,7 @@ impl RemoteFsBuilder {
|
||||
credentials = credentials.password(password);
|
||||
}
|
||||
|
||||
SmbFs::new(credentials)
|
||||
Ok(SmbFs::new(credentials))
|
||||
}
|
||||
|
||||
fn webdav_client(params: WebDAVProtocolParams) -> WebDAVFs {
|
||||
@@ -236,12 +231,13 @@ impl RemoteFsBuilder {
|
||||
} else {
|
||||
//* case 3: use system username; can't be None
|
||||
debug!("no username was provided, using current username");
|
||||
opts = opts.username(whoami::username());
|
||||
if let Ok(username) = whoami::username() {
|
||||
opts = opts.username(username);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//* case 3: use system username; can't be None
|
||||
} else if let Ok(username) = whoami::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.
|
||||
// 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 {
|
||||
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)]
|
||||
|
||||
@@ -19,7 +19,6 @@ pub type HostResult<T> = Result<T, HostError>;
|
||||
|
||||
/// HostErrorType provides an overview of the specific host error
|
||||
#[derive(Error, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum HostErrorType {
|
||||
#[error("No such file or directory")]
|
||||
NoSuchFileOrDirectory,
|
||||
@@ -37,6 +36,7 @@ pub enum HostErrorType {
|
||||
ExecutionFailed,
|
||||
#[error("Could not delete file")]
|
||||
DeleteFailed,
|
||||
#[cfg(win)]
|
||||
#[error("Not implemented")]
|
||||
NotImplemented,
|
||||
#[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::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::io::{Read, Write};
|
||||
#[cfg(posix)]
|
||||
@@ -347,6 +352,8 @@ impl HostBridge for Localhost {
|
||||
}
|
||||
|
||||
fn exists(&mut self, path: &Path) -> HostResult<bool> {
|
||||
let path: PathBuf = self.to_path(path);
|
||||
|
||||
Ok(path.exists())
|
||||
}
|
||||
|
||||
@@ -395,7 +402,17 @@ impl HostBridge for Localhost {
|
||||
fn exec(&mut self, cmd: &str) -> HostResult<String> {
|
||||
// Make command
|
||||
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..];
|
||||
info!("Executing command: {} {:?}", cmd, argv);
|
||||
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());
|
||||
}
|
||||
|
||||
#[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)]
|
||||
#[test]
|
||||
fn test_host_localhost_symlinks() {
|
||||
@@ -993,6 +1026,14 @@ mod tests {
|
||||
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)]
|
||||
#[test]
|
||||
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]
|
||||
fn test_host_fmt_error() {
|
||||
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;
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use remotefs::fs::{Metadata, UnixPex};
|
||||
use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs};
|
||||
|
||||
use self::temp_mapped_file::TempMappedFile;
|
||||
use super::{HostBridge, HostError, HostResult};
|
||||
use crate::utils::path::normalize;
|
||||
|
||||
struct WriteStreamOp {
|
||||
path: PathBuf,
|
||||
@@ -124,7 +130,8 @@ impl HostBridge for RemoteBridged {
|
||||
|
||||
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
|
||||
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<()> {
|
||||
@@ -209,3 +216,100 @@ impl HostBridge for RemoteBridged {
|
||||
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::io::{self, Read, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -16,27 +21,25 @@ pub struct TempMappedFile {
|
||||
|
||||
impl Write for TempMappedFile {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let rc = self.write_hnd()?;
|
||||
let mut ref_mut = rc.lock().unwrap();
|
||||
ref_mut.as_mut().unwrap().write(buf)
|
||||
let mut handle = self.write_hnd()?;
|
||||
handle.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
let rc = self.write_hnd()?;
|
||||
let mut ref_mut = rc.lock().unwrap();
|
||||
ref_mut.as_mut().unwrap().flush()
|
||||
let mut handle = self.write_hnd()?;
|
||||
handle.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for TempMappedFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let rc = self.read_hnd()?;
|
||||
let mut ref_mut = rc.lock().unwrap();
|
||||
ref_mut.as_mut().unwrap().read(buf)
|
||||
let mut handle = self.read_hnd()?;
|
||||
handle.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl TempMappedFile {
|
||||
/// Creates an empty temporary file container for a downloaded remote file.
|
||||
pub fn new() -> HostResult<Self> {
|
||||
NamedTempFile::new()
|
||||
.map(|tempfile| TempMappedFile {
|
||||
@@ -57,7 +60,13 @@ impl TempMappedFile {
|
||||
/// Must be called
|
||||
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() {
|
||||
hnd.sync_all().map_err(|e| {
|
||||
@@ -73,28 +82,62 @@ impl TempMappedFile {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_hnd(&mut self) -> io::Result<Arc<Mutex<Option<File>>>> {
|
||||
{
|
||||
let mut lock = self.handle.lock().unwrap();
|
||||
if lock.is_none() {
|
||||
let hnd = File::create(self.tempfile.path())?;
|
||||
lock.replace(hnd);
|
||||
}
|
||||
fn write_hnd(&mut self) -> io::Result<FileHandle<'_>> {
|
||||
let mut lock = self.lock_handle()?;
|
||||
if lock.is_none() {
|
||||
let hnd = File::create(self.tempfile.path())?;
|
||||
lock.replace(hnd);
|
||||
}
|
||||
|
||||
Ok(self.handle.clone())
|
||||
Ok(FileHandle::new(lock))
|
||||
}
|
||||
|
||||
fn read_hnd(&mut self) -> io::Result<Arc<Mutex<Option<File>>>> {
|
||||
{
|
||||
let mut lock = self.handle.lock().unwrap();
|
||||
if lock.is_none() {
|
||||
let hnd = File::open(self.tempfile.path())?;
|
||||
lock.replace(hnd);
|
||||
}
|
||||
fn read_hnd(&mut self) -> io::Result<FileHandle<'_>> {
|
||||
let mut lock = self.lock_handle()?;
|
||||
if lock.is_none() {
|
||||
let hnd = File::open(self.tempfile.path())?;
|
||||
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 cli;
|
||||
mod config;
|
||||
@@ -15,11 +20,7 @@ extern crate bitflags;
|
||||
#[macro_use]
|
||||
extern crate lazy_regex;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate magic_crypt;
|
||||
|
||||
use std::env;
|
||||
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::io::BufReader;
|
||||
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)
|
||||
.for_each(|(name, params, identity_file_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
|
||||
if let Some(identity_file_params) = identity_file_params {
|
||||
@@ -255,8 +262,7 @@ mod tests {
|
||||
|
||||
struct SshTestConfig {
|
||||
config: NamedTempFile,
|
||||
#[allow(dead_code)]
|
||||
identity_file: NamedTempFile,
|
||||
_identity_file: NamedTempFile,
|
||||
}
|
||||
|
||||
fn ssh_test_config() -> SshTestConfig {
|
||||
@@ -320,7 +326,7 @@ Host test3
|
||||
|
||||
SshTestConfig {
|
||||
config: file,
|
||||
identity_file,
|
||||
_identity_file: identity_file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ pub enum UpdateStatus {
|
||||
/// Info related to a github release
|
||||
#[derive(Debug)]
|
||||
pub struct Release {
|
||||
/// Release version string returned by GitHub.
|
||||
pub version: String,
|
||||
/// Release notes body returned by GitHub.
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
@@ -48,6 +50,7 @@ impl Update {
|
||||
self
|
||||
}
|
||||
|
||||
/// Installs the latest available release using the configured update options.
|
||||
pub fn upgrade(self) -> Result<UpdateStatus, UpdateError> {
|
||||
info!("Updating termscp...");
|
||||
GithubUpdater::configure()
|
||||
@@ -132,8 +135,13 @@ impl Update {
|
||||
|
||||
/// Check wether new version is higher than new version
|
||||
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)
|
||||
== version_compare::Cmp::Gt
|
||||
match (
|
||||
semver::Version::parse(new_version),
|
||||
semver::Version::parse(current_version),
|
||||
) {
|
||||
(Ok(new), Ok(current)) => new > current,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Status> for UpdateStatus {
|
||||
@@ -230,15 +238,64 @@ mod test {
|
||||
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]
|
||||
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.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.9", "0.10.1"));
|
||||
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]
|
||||
fn test_should_check_whether_github_api_is_reachable() {
|
||||
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
|
||||
|
||||
// Crate
|
||||
// Ext
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::string::ToString;
|
||||
@@ -12,11 +10,8 @@ use std::time::SystemTime;
|
||||
use super::keys::filestorage::FileStorage;
|
||||
use super::keys::keyringstorage::KeyringStorage;
|
||||
use super::keys::{KeyStorage, KeyStorageError};
|
||||
// Local
|
||||
use crate::config::{
|
||||
bookmarks::{Bookmark, UserHosts},
|
||||
serialization::{SerializerError, SerializerErrorKind, deserialize, serialize},
|
||||
};
|
||||
use crate::config::bookmarks::{Bookmark, UserHosts};
|
||||
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::utils::crypto;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
@@ -104,7 +99,7 @@ impl BookmarksClient {
|
||||
fn keyring(storage_path: &Path, keyring: bool) -> (Box<dyn KeyStorage>, &'static str) {
|
||||
if keyring && cfg!(feature = "keyring") {
|
||||
debug!("Setting up KeyStorage");
|
||||
let username: String = whoami::username();
|
||||
let username = whoami::username().unwrap_or_default();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
// Check if keyring storage is supported
|
||||
#[cfg(not(test))]
|
||||
@@ -192,15 +187,15 @@ impl BookmarksClient {
|
||||
name: S,
|
||||
params: FileTransferParams,
|
||||
save_password: bool,
|
||||
) {
|
||||
) -> Result<(), SerializerError> {
|
||||
let name: String = name.as_ref().to_string();
|
||||
if name.is_empty() {
|
||||
error!("Fatal error; bookmark name is empty");
|
||||
panic!("Bookmark name can't be empty");
|
||||
error!("Bookmark name is empty; ignoring add_bookmark request");
|
||||
return Ok(());
|
||||
}
|
||||
// Make bookmark
|
||||
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 !save_password {
|
||||
host.password = None;
|
||||
@@ -210,6 +205,7 @@ impl BookmarksClient {
|
||||
}
|
||||
}
|
||||
self.hosts.bookmarks.insert(name, host);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete entry from bookmarks
|
||||
@@ -231,9 +227,9 @@ impl BookmarksClient {
|
||||
}
|
||||
|
||||
/// 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
|
||||
let mut host: Bookmark = self.make_bookmark(params);
|
||||
let mut host: Bookmark = self.make_bookmark(params)?;
|
||||
// Null password for recents
|
||||
host.password = None;
|
||||
if let Some(s3) = host.s3.as_mut() {
|
||||
@@ -245,7 +241,7 @@ impl BookmarksClient {
|
||||
if *value == host {
|
||||
debug!("Discarding recent since duplicated ({})", key);
|
||||
// Don't save duplicates
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// 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");
|
||||
info!("Saved recent host {}", name);
|
||||
self.hosts.recents.insert(name, host);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete entry from recents
|
||||
@@ -334,27 +331,29 @@ impl BookmarksClient {
|
||||
}
|
||||
|
||||
/// 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);
|
||||
// Encrypt 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
|
||||
if let Some(s3) = bookmark.s3.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() {
|
||||
*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
|
||||
fn encrypt_str(&self, txt: &str) -> String {
|
||||
crypto::aes128_b64_crypt(self.key.as_str(), txt)
|
||||
fn encrypt_str(&self, txt: &str) -> Result<String, SerializerError> {
|
||||
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
|
||||
@@ -408,24 +407,32 @@ mod tests {
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add some bookmarks
|
||||
client.add_bookmark(
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
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());
|
||||
assert!(client.write_bookmarks().is_ok());
|
||||
let key: String = client.key.clone();
|
||||
@@ -456,7 +463,11 @@ mod tests {
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add s3 bookmark
|
||||
client.add_bookmark("my-bucket", make_s3_ftparams(), true);
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark("my-bucket", make_s3_ftparams(), true)
|
||||
.is_ok()
|
||||
);
|
||||
// Verify bookmark
|
||||
let bookmark = client.get_bookmark("my-bucket").unwrap();
|
||||
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
||||
@@ -476,7 +487,11 @@ mod tests {
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add s3 bookmark
|
||||
client.add_bookmark("my-bucket", make_s3_ftparams(), false);
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark("my-bucket", make_s3_ftparams(), false)
|
||||
.is_ok()
|
||||
);
|
||||
// Verify bookmark
|
||||
let bookmark = client.get_bookmark("my-bucket").unwrap();
|
||||
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
||||
@@ -497,7 +512,7 @@ mod tests {
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add s3 bookmark
|
||||
client.add_recent(make_s3_ftparams());
|
||||
assert!(client.add_recent(make_s3_ftparams()).is_ok());
|
||||
// Verify bookmark
|
||||
let bookmark = client.iter_recents().next().unwrap();
|
||||
let bookmark = client.get_recent(bookmark).unwrap();
|
||||
@@ -520,27 +535,35 @@ mod tests {
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
client.add_bookmark(
|
||||
"raspberry2",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword2"),
|
||||
),
|
||||
true,
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"raspberry2",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword2"),
|
||||
),
|
||||
true,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
// Iter
|
||||
assert_eq!(client.iter_bookmarks().count(), 2);
|
||||
@@ -562,26 +585,30 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
|
||||
fn test_system_bookmarks_bad_bookmark_name() {
|
||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
// Add bookmark with empty name should be silently ignored
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
// No bookmark should have been added
|
||||
assert_eq!(client.iter_bookmarks().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -592,16 +619,20 @@ mod tests {
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
false,
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
false,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
@@ -620,13 +651,17 @@ mod tests {
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
// Iter
|
||||
assert_eq!(client.iter_recents().count(), 1);
|
||||
let key: String = String::from(client.iter_recents().next().unwrap());
|
||||
@@ -656,20 +691,28 @@ mod tests {
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
// There should be only one recent
|
||||
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();
|
||||
// Add recent, wait 1 second for each one (cause the name depends on time)
|
||||
// 1
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.1",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.1",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
sleep(Duration::from_secs(1));
|
||||
// 2
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.2",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.2",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
sleep(Duration::from_secs(1));
|
||||
// 3
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.3",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.3",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
// Limit is 2
|
||||
assert_eq!(client.iter_recents().count(), 2);
|
||||
// Check that 192.168.1.1 has been removed
|
||||
@@ -743,25 +798,30 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_system_bookmarks_add_bookmark_empty() {
|
||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
// Add bookmark with empty name should be silently ignored
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
// No bookmark should have been added
|
||||
assert_eq!(client.iter_bookmarks().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -779,6 +839,59 @@ mod tests {
|
||||
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
|
||||
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||
let k: PathBuf = PathBuf::from(dir);
|
||||
|
||||
@@ -309,7 +309,7 @@ impl ConfigClient {
|
||||
None => None,
|
||||
Some(key_path) => {
|
||||
// Get host and username
|
||||
let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
|
||||
let (host, username) = Self::get_ssh_tokens(mkey)?;
|
||||
// Return key
|
||||
Some((host, username, PathBuf::from(key_path)))
|
||||
}
|
||||
@@ -389,12 +389,18 @@ impl ConfigClient {
|
||||
}
|
||||
|
||||
/// Get ssh tokens starting from ssh host key
|
||||
/// Panics if key has invalid syntax
|
||||
/// Returns: (host, username)
|
||||
fn get_ssh_tokens(host_key: &str) -> (String, String) {
|
||||
/// Returns: (host, username) or None if key has invalid syntax
|
||||
fn get_ssh_tokens(host_key: &str) -> Option<(String, String)> {
|
||||
let tokens: Vec<&str> = host_key.split('@').collect();
|
||||
assert!(tokens.len() >= 2);
|
||||
(String::from(tokens[1]), String::from(tokens[0]))
|
||||
if tokens.len() >= 2 {
|
||||
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`
|
||||
@@ -711,10 +717,16 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
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]
|
||||
fn test_system_config_make_io_err() {
|
||||
let err: SerializerError =
|
||||
|
||||
+11
-20
@@ -4,20 +4,21 @@
|
||||
|
||||
// Ext
|
||||
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.
|
||||
/// Returns None if it's not possible to initialize it
|
||||
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() {
|
||||
init_dir(dir).map(Option::Some)
|
||||
} else {
|
||||
@@ -28,16 +29,6 @@ pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
|
||||
/// Get termscp cache directory path and initialize it.
|
||||
/// Returns None if it's not possible to initialize it
|
||||
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() {
|
||||
init_dir(dir).map(Option::Some)
|
||||
} else {
|
||||
|
||||
@@ -13,7 +13,7 @@ use thiserror::Error;
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KeyStorageError {
|
||||
#[error("Key has a bad syntax")]
|
||||
BadSytax,
|
||||
BadSyntax,
|
||||
#[error("Provider service error")]
|
||||
ProviderError,
|
||||
#[error("No such key")]
|
||||
@@ -54,7 +54,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_system_keys_mod_errors() {
|
||||
assert_eq!(
|
||||
KeyStorageError::BadSytax.to_string(),
|
||||
KeyStorageError::BadSyntax.to_string(),
|
||||
String::from("Key has a bad syntax")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -71,7 +71,10 @@ impl KeyStorage for FileStorage {
|
||||
return Err(KeyStorageError::ProviderError);
|
||||
}
|
||||
// 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);
|
||||
let _ = file.set_permissions(permissions);
|
||||
Ok(())
|
||||
|
||||
@@ -37,7 +37,7 @@ impl KeyStorage for KeyringStorage {
|
||||
| KeyringError::Invalid(_, _)
|
||||
| KeyringError::Ambiguous(_) => Err(KeyStorageError::ProviderError),
|
||||
KeyringError::BadEncoding(_) | KeyringError::TooLong(_, _) => {
|
||||
Err(KeyStorageError::BadSytax)
|
||||
Err(KeyStorageError::BadSyntax)
|
||||
}
|
||||
_ => Err(KeyStorageError::ProviderError),
|
||||
},
|
||||
@@ -78,14 +78,13 @@ impl KeyStorage for KeyringStorage {
|
||||
mod tests {
|
||||
#[test]
|
||||
#[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 whoami::username;
|
||||
|
||||
use super::*;
|
||||
|
||||
let username: String = username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
let username = whoami::username().expect("no username");
|
||||
let storage = KeyringStorage::new(username.as_str());
|
||||
assert!(storage.is_supported());
|
||||
let app_name: &str = "termscp-test2";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
|
||||
@@ -13,6 +13,8 @@ use ssh2_config::SshConfig;
|
||||
use super::config_client::ConfigClient;
|
||||
use crate::utils::ssh as ssh_utils;
|
||||
|
||||
/// Resolves SSH identity files from termscp config, SSH config, and standard
|
||||
/// OpenSSH default locations.
|
||||
#[derive(Default)]
|
||||
pub struct SshKeyStorage {
|
||||
/// Association between {user}@{host} and RSA key path
|
||||
@@ -38,7 +40,7 @@ impl SshKeyStorage {
|
||||
/// Resolve host via termscp ssh keys storage
|
||||
fn resolve_host_in_termscp_storage(&self, host: &str, username: &str) -> Option<&Path> {
|
||||
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
|
||||
@@ -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
|
||||
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||
let mut k: PathBuf = PathBuf::from(dir);
|
||||
|
||||
@@ -23,7 +23,7 @@ impl ThemeProvider {
|
||||
pub fn new(theme_path: &Path) -> Result<Self, SerializerError> {
|
||||
let default_theme: Theme = Theme::default();
|
||||
info!(
|
||||
"Setting up theme provider with thene path {} ",
|
||||
"Setting up theme provider with theme path {}",
|
||||
theme_path.display(),
|
||||
);
|
||||
// Create provider
|
||||
@@ -42,7 +42,7 @@ impl ThemeProvider {
|
||||
} else {
|
||||
// otherwise Load configuration from file
|
||||
if let Err(err) = provider.load() {
|
||||
error!("Couldn't read thene file: {}", err);
|
||||
error!("Couldn't read theme file: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Read theme file");
|
||||
|
||||
@@ -27,6 +27,8 @@ pub enum FsWatcherError {
|
||||
PathNotWatched,
|
||||
#[error("unable to watch path, since it's already watched")]
|
||||
PathAlreadyWatched,
|
||||
#[error("watcher event channel disconnected")]
|
||||
Disconnected,
|
||||
#[error("unknown event: {0}")]
|
||||
UnknownEvent(&'static str),
|
||||
#[error("worker error: {0}")]
|
||||
@@ -114,7 +116,7 @@ impl FsWatcher {
|
||||
let res = match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
||||
Ok(res) => res,
|
||||
Err(RecvTimeoutError::Timeout) => return Ok(None),
|
||||
Err(RecvTimeoutError::Disconnected) => panic!("File watcher died"),
|
||||
Err(RecvTimeoutError::Disconnected) => return Err(FsWatcherError::Disconnected),
|
||||
};
|
||||
|
||||
// convert event to FsChange
|
||||
@@ -158,7 +160,7 @@ impl FsWatcher {
|
||||
|
||||
/// Returns the list of watched paths
|
||||
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.
|
||||
@@ -13,9 +13,10 @@ mod view;
|
||||
// Includes
|
||||
use std::time::Duration;
|
||||
|
||||
use tuirealm::application::PollStrategy;
|
||||
use tuirealm::application::{Application, PollStrategy};
|
||||
use tuirealm::event::NoUserEvent;
|
||||
use tuirealm::listener::EventListenerCfg;
|
||||
use tuirealm::{Application, NoUserEvent, Update};
|
||||
use tuirealm::terminal::TerminalAdapter;
|
||||
|
||||
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
@@ -254,9 +255,7 @@ impl AuthActivity {
|
||||
pub fn new(ticks: Duration) -> AuthActivity {
|
||||
AuthActivity {
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL)
|
||||
.poll_timeout(ticks),
|
||||
EventListenerCfg::default().crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
||||
),
|
||||
context: None,
|
||||
bookmarks_list: Vec::new(),
|
||||
@@ -369,7 +368,10 @@ impl Activity for AuthActivity {
|
||||
return;
|
||||
}
|
||||
// Tick
|
||||
match self.app.tick(PollStrategy::UpTo(3)) {
|
||||
match self
|
||||
.app
|
||||
.tick(PollStrategy::UpTo(3, std::time::Duration::from_millis(10)))
|
||||
{
|
||||
Ok(messages) => {
|
||||
for msg in messages.into_iter() {
|
||||
let mut msg = Some(msg);
|
||||
@@ -71,7 +71,10 @@ impl AuthActivity {
|
||||
};
|
||||
|
||||
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
|
||||
self.write_bookmarks();
|
||||
// Remove `name` from bookmarks if exists
|
||||
@@ -121,7 +124,10 @@ impl AuthActivity {
|
||||
}
|
||||
};
|
||||
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
|
||||
self.write_bookmarks();
|
||||
}
|
||||
|
||||
@@ -28,19 +28,19 @@ pub use popup::{
|
||||
WindowSizeError,
|
||||
};
|
||||
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::{Component, MockComponent};
|
||||
|
||||
// -- global listener
|
||||
|
||||
#[derive(Default, MockComponent)]
|
||||
#[derive(Default, Component)]
|
||||
pub struct GlobalListener {
|
||||
component: Phantom,
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for GlobalListener {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for GlobalListener {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Function(10),
|
||||
@@ -2,18 +2,22 @@
|
||||
//!
|
||||
//! 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::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputType, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
use tuirealm::component::{AppComponent, Component};
|
||||
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
|
||||
use tuirealm::props::{
|
||||
BorderSides, BorderType, Borders, Color, HorizontalAlignment, InputType, SpanStatic, Style,
|
||||
TextModifiers, Title,
|
||||
};
|
||||
use tuirealm::state::{State, StateValue};
|
||||
|
||||
use super::{FormMsg, Msg, UiMsg};
|
||||
use crate::ui::activities::auth::FormTab;
|
||||
|
||||
// -- bookmark list
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct BookmarksList {
|
||||
component: List,
|
||||
}
|
||||
@@ -23,23 +27,23 @@ impl BookmarksList {
|
||||
Self {
|
||||
component: List::default()
|
||||
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
||||
.highlighted_color(color)
|
||||
.highlight_style(Style::default().fg(color))
|
||||
.rewind(true)
|
||||
.scroll(true)
|
||||
.step(4)
|
||||
.title("Bookmarks", Alignment::Left)
|
||||
.title(Title::from("Bookmarks").alignment(HorizontalAlignment::Left))
|
||||
.rows(
|
||||
bookmarks
|
||||
.iter()
|
||||
.map(|x| vec![TextSpan::from(x.as_str())])
|
||||
.collect(),
|
||||
.map(|x| vec![SpanStatic::from(x.clone())])
|
||||
.collect::<Vec<Vec<SpanStatic>>>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for BookmarksList {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for BookmarksList {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
@@ -77,7 +81,7 @@ impl Component<Msg, NoUserEvent> for BookmarksList {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.state() {
|
||||
State::One(StateValue::Usize(choice)) => {
|
||||
State::Single(StateValue::Usize(choice)) => {
|
||||
Some(Msg::Form(FormMsg::LoadBookmark(choice)))
|
||||
}
|
||||
_ => Some(Msg::None),
|
||||
@@ -98,7 +102,7 @@ impl Component<Msg, NoUserEvent> for BookmarksList {
|
||||
|
||||
// -- recents list
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct RecentsList {
|
||||
component: List,
|
||||
}
|
||||
@@ -108,23 +112,23 @@ impl RecentsList {
|
||||
Self {
|
||||
component: List::default()
|
||||
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
||||
.highlighted_color(color)
|
||||
.highlight_style(Style::default().fg(color))
|
||||
.rewind(true)
|
||||
.scroll(true)
|
||||
.step(4)
|
||||
.title("Recent connections", Alignment::Left)
|
||||
.title(Title::from("Recent connections").alignment(HorizontalAlignment::Left))
|
||||
.rows(
|
||||
bookmarks
|
||||
.iter()
|
||||
.map(|x| vec![TextSpan::from(x.as_str())])
|
||||
.collect(),
|
||||
.map(|x| vec![SpanStatic::from(x.clone())])
|
||||
.collect::<Vec<Vec<SpanStatic>>>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for RecentsList {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for RecentsList {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
@@ -162,7 +166,7 @@ impl Component<Msg, NoUserEvent> for RecentsList {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.state() {
|
||||
State::One(StateValue::Usize(choice)) => {
|
||||
State::Single(StateValue::Usize(choice)) => {
|
||||
Some(Msg::Form(FormMsg::LoadRecent(choice)))
|
||||
}
|
||||
_ => Some(Msg::None),
|
||||
@@ -183,7 +187,7 @@ impl Component<Msg, NoUserEvent> for RecentsList {
|
||||
|
||||
// -- delete bookmark
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct DeleteBookmarkPopup {
|
||||
component: Radio,
|
||||
}
|
||||
@@ -192,6 +196,11 @@ impl DeleteBookmarkPopup {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(color)
|
||||
.add_modifier(TextModifiers::REVERSED),
|
||||
)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
@@ -200,14 +209,15 @@ impl DeleteBookmarkPopup {
|
||||
.choices(["Yes", "No"])
|
||||
.value(1)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Delete selected bookmark?", Alignment::Center),
|
||||
.title(
|
||||
Title::from("Delete selected bookmark?").alignment(HorizontalAlignment::Center),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseDeleteBookmark))
|
||||
@@ -237,7 +247,7 @@ impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
CmdResult::Submit(State::Single(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::Form(FormMsg::DeleteBookmark))
|
||||
} else {
|
||||
@@ -251,7 +261,7 @@ impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
||||
|
||||
// -- delete recent
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct DeleteRecentPopup {
|
||||
component: Radio,
|
||||
}
|
||||
@@ -260,6 +270,11 @@ impl DeleteRecentPopup {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(color)
|
||||
.add_modifier(TextModifiers::REVERSED),
|
||||
)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
@@ -268,14 +283,16 @@ impl DeleteRecentPopup {
|
||||
.choices(["Yes", "No"])
|
||||
.value(1)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Delete selected recent host?", Alignment::Center),
|
||||
.title(
|
||||
Title::from("Delete selected recent host?")
|
||||
.alignment(HorizontalAlignment::Center),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseDeleteRecent))
|
||||
@@ -305,7 +322,7 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
CmdResult::Submit(State::Single(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::Form(FormMsg::DeleteRecent))
|
||||
} else {
|
||||
@@ -321,7 +338,7 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
|
||||
// -- save password
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct BookmarkSavePassword {
|
||||
component: Radio,
|
||||
form_tab: FormTab,
|
||||
@@ -331,6 +348,11 @@ impl BookmarkSavePassword {
|
||||
pub fn new(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::Reset)
|
||||
@@ -340,15 +362,14 @@ impl BookmarkSavePassword {
|
||||
.choices(["Yes", "No"])
|
||||
.value(0)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Save secrets?", Alignment::Center),
|
||||
.title(Title::from("Save secrets?").alignment(HorizontalAlignment::Center)),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseSaveBookmark))
|
||||
@@ -378,7 +399,7 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
|
||||
// -- new bookmark name
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct BookmarkName {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
@@ -395,15 +416,15 @@ impl BookmarkName {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Bookmark name", Alignment::Left)
|
||||
.title(Title::from("Bookmark name").alignment(HorizontalAlignment::Left))
|
||||
.input_type(InputType::Text),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for BookmarkName {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for BookmarkName {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseSaveBookmark))
|
||||
@@ -447,7 +468,7 @@ impl Component<Msg, NoUserEvent> for BookmarkName {
|
||||
code: Key::Char(ch),
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
self.perform(Cmd::Type(*ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
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
|
||||
|
||||
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::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
use tuirealm::component::{AppComponent, Component};
|
||||
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
|
||||
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};
|
||||
|
||||
// -- error popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct ErrorPopup {
|
||||
component: Paragraph,
|
||||
}
|
||||
@@ -21,21 +27,23 @@ impl ErrorPopup {
|
||||
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||
Self {
|
||||
component: Paragraph::default()
|
||||
.alignment(Alignment::Center)
|
||||
.alignment_horizontal(HorizontalAlignment::Center)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
.text(Text::from_iter([SpanStatic::from(
|
||||
text.as_ref().to_string(),
|
||||
)]))
|
||||
.wrap_trim(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ErrorPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for ErrorPopup {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Enter,
|
||||
@@ -48,7 +56,7 @@ impl Component<Msg, NoUserEvent> for ErrorPopup {
|
||||
|
||||
// -- info popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct InfoPopup {
|
||||
component: Paragraph,
|
||||
}
|
||||
@@ -57,21 +65,23 @@ impl InfoPopup {
|
||||
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||
Self {
|
||||
component: Paragraph::default()
|
||||
.alignment(Alignment::Center)
|
||||
.alignment_horizontal(HorizontalAlignment::Center)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
.text(Text::from_iter([SpanStatic::from(
|
||||
text.as_ref().to_string(),
|
||||
)]))
|
||||
.wrap_trim(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InfoPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for InfoPopup {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Enter,
|
||||
@@ -84,7 +94,7 @@ impl Component<Msg, NoUserEvent> for InfoPopup {
|
||||
|
||||
// -- wait popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct WaitPopup {
|
||||
component: Paragraph,
|
||||
}
|
||||
@@ -93,28 +103,30 @@ impl WaitPopup {
|
||||
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||
Self {
|
||||
component: Paragraph::default()
|
||||
.alignment(Alignment::Center)
|
||||
.alignment_horizontal(HorizontalAlignment::Center)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
.text(Text::from_iter([SpanStatic::from(
|
||||
text.as_ref().to_string(),
|
||||
)]))
|
||||
.wrap_trim(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for WaitPopup {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for WaitPopup {
|
||||
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- window size error
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct WindowSizeError {
|
||||
component: Paragraph,
|
||||
}
|
||||
@@ -123,30 +135,30 @@ impl WindowSizeError {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Paragraph::default()
|
||||
.alignment(Alignment::Center)
|
||||
.alignment_horizontal(HorizontalAlignment::Center)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text([TextSpan::from(
|
||||
.text(Text::from_iter([SpanStatic::from(
|
||||
"termscp requires at least 24 lines of height to run",
|
||||
)])
|
||||
.wrap(true),
|
||||
)]))
|
||||
.wrap_trim(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for WindowSizeError {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for WindowSizeError {
|
||||
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- quit popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct QuitPopup {
|
||||
component: Radio,
|
||||
}
|
||||
@@ -155,21 +167,25 @@ impl QuitPopup {
|
||||
pub fn new(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),
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Quit termscp?", Alignment::Center)
|
||||
.title(Title::from("Quit termscp?").alignment(HorizontalAlignment::Center))
|
||||
.rewind(true)
|
||||
.choices(["Yes", "No"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for QuitPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for QuitPopup {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseQuitPopup))
|
||||
@@ -199,7 +215,7 @@ impl Component<Msg, NoUserEvent> for QuitPopup {
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
CmdResult::Submit(State::Single(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::Form(FormMsg::Quit))
|
||||
} else {
|
||||
@@ -213,7 +229,7 @@ impl Component<Msg, NoUserEvent> for QuitPopup {
|
||||
|
||||
// -- install update popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct InstallUpdatePopup {
|
||||
component: Radio,
|
||||
}
|
||||
@@ -222,21 +238,25 @@ impl InstallUpdatePopup {
|
||||
pub fn new(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),
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Install update?", Alignment::Center)
|
||||
.title(Title::from("Install update?").alignment(HorizontalAlignment::Center))
|
||||
.rewind(true)
|
||||
.choices(["Yes", "No"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for InstallUpdatePopup {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseInstallUpdatePopup))
|
||||
@@ -266,7 +286,7 @@ impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
CmdResult::Submit(State::Single(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::Form(FormMsg::InstallUpdate))
|
||||
} else {
|
||||
@@ -280,7 +300,7 @@ impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
|
||||
|
||||
// -- release notes popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct ReleaseNotes {
|
||||
component: Textarea,
|
||||
}
|
||||
@@ -295,14 +315,14 @@ impl ReleaseNotes {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Release notes", Alignment::Center)
|
||||
.text_rows(notes.lines().map(TextSpan::from)),
|
||||
.title(Title::from("Release notes").alignment(HorizontalAlignment::Center))
|
||||
.text_rows(notes.lines().map(|l| SpanStatic::from(l.to_string()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for ReleaseNotes {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for ReleaseNotes {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Enter,
|
||||
@@ -348,7 +368,7 @@ impl Component<Msg, NoUserEvent> for ReleaseNotes {
|
||||
|
||||
// -- keybindings popup
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct Keybindings {
|
||||
component: List,
|
||||
}
|
||||
@@ -362,43 +382,47 @@ impl Keybindings {
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.highlighted_str("? ")
|
||||
.title("Keybindings", Alignment::Center)
|
||||
.highlight_str("? ")
|
||||
.title(Title::from("Keybindings").alignment(HorizontalAlignment::Center))
|
||||
.scroll(true)
|
||||
.step(4)
|
||||
.rows(
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::new("<ESC>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Quit termscp"))
|
||||
.add_col(SpanStatic::raw("<ESC>").bold().fg(color))
|
||||
.add_col(SpanStatic::from(" Quit termscp"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<TAB>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Switch from form and bookmarks"))
|
||||
.add_col(SpanStatic::raw("<TAB>").bold().fg(color))
|
||||
.add_col(SpanStatic::from(
|
||||
" Switch from form and bookmarks",
|
||||
))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Switch bookmark tab"))
|
||||
.add_col(SpanStatic::raw("<RIGHT/LEFT>").bold().fg(color))
|
||||
.add_col(SpanStatic::from(" Switch bookmark tab"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Move up/down in current tab"))
|
||||
.add_col(SpanStatic::raw("<UP/DOWN>").bold().fg(color))
|
||||
.add_col(SpanStatic::from(" Move up/down in current tab"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<ENTER>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Connect/Load bookmark"))
|
||||
.add_col(SpanStatic::raw("<ENTER>").bold().fg(color))
|
||||
.add_col(SpanStatic::from(" Connect/Load bookmark"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<DEL|E>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Delete selected bookmark"))
|
||||
.add_col(SpanStatic::raw("<DEL|E>").bold().fg(color))
|
||||
.add_col(SpanStatic::from(" Delete selected bookmark"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+C>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Enter setup"))
|
||||
.add_col(SpanStatic::raw("<CTRL+C>").bold().fg(color))
|
||||
.add_col(SpanStatic::from(" Enter setup"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+S>").bold().fg(color))
|
||||
.add_col(TextSpan::from(" Save bookmark"))
|
||||
.build(),
|
||||
.add_col(SpanStatic::raw("<CTRL+S>").bold().fg(color))
|
||||
.add_col(SpanStatic::from(" Save bookmark"))
|
||||
.build()
|
||||
.into_iter()
|
||||
.map(|row| row.into_iter().flat_map(|l| l.spans).collect::<Vec<_>>()),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for Keybindings {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for Keybindings {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Enter,
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
//!
|
||||
//! auth activity texts
|
||||
|
||||
use tui_realm_stdlib::{Label, Span};
|
||||
use tuirealm::props::{Color, TextModifiers, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||
use tui_realm_stdlib::components::{Label, Span};
|
||||
use tuirealm::component::{AppComponent, Component};
|
||||
use tuirealm::event::{Event, NoUserEvent};
|
||||
use tuirealm::props::{Color, SpanStatic, TextModifiers};
|
||||
use tuirealm::ratatui::style::Stylize;
|
||||
|
||||
use super::Msg;
|
||||
|
||||
// -- Title
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct Title {
|
||||
component: Label,
|
||||
}
|
||||
@@ -25,15 +27,15 @@ impl Default for Title {
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for Title {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for Title {
|
||||
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- subtitle
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct Subtitle {
|
||||
component: Label,
|
||||
}
|
||||
@@ -48,15 +50,15 @@ impl Default for Subtitle {
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for Subtitle {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for Subtitle {
|
||||
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- new version disclaimer
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct NewVersionDisclaimer {
|
||||
component: Span,
|
||||
}
|
||||
@@ -65,9 +67,9 @@ impl NewVersionDisclaimer {
|
||||
pub fn new(new_version: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().foreground(color).spans([
|
||||
TextSpan::from("termscp "),
|
||||
TextSpan::new(new_version).underlined().bold(),
|
||||
TextSpan::from(
|
||||
SpanStatic::from("termscp "),
|
||||
SpanStatic::raw(new_version.to_string()).underlined().bold(),
|
||||
SpanStatic::from(
|
||||
" is NOW available! Install update and view release notes with <CTRL+R>",
|
||||
),
|
||||
]),
|
||||
@@ -75,15 +77,15 @@ impl NewVersionDisclaimer {
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for NewVersionDisclaimer {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for NewVersionDisclaimer {
|
||||
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- HelpFooter
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct HelpFooter {
|
||||
component: Span,
|
||||
}
|
||||
@@ -92,27 +94,27 @@ impl HelpFooter {
|
||||
pub fn new(key_color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().spans([
|
||||
TextSpan::from("<F1|CTRL+H>").bold().fg(key_color),
|
||||
TextSpan::from(" Help "),
|
||||
TextSpan::from("<CTRL+C>").bold().fg(key_color),
|
||||
TextSpan::from(" Enter setup "),
|
||||
TextSpan::from("<UP/DOWN>").bold().fg(key_color),
|
||||
TextSpan::from(" Change field "),
|
||||
TextSpan::from("<TAB>").bold().fg(key_color),
|
||||
TextSpan::from(" Switch tab "),
|
||||
TextSpan::from("<BACKTAB>").bold().fg(key_color),
|
||||
TextSpan::from(" Switch form "),
|
||||
TextSpan::from("<ENTER>").bold().fg(key_color),
|
||||
TextSpan::from(" Submit form "),
|
||||
TextSpan::from("<F10|ESC>").bold().fg(key_color),
|
||||
TextSpan::from(" Quit "),
|
||||
SpanStatic::from("<F1|CTRL+H>").bold().fg(key_color),
|
||||
SpanStatic::from(" Help "),
|
||||
SpanStatic::from("<CTRL+C>").bold().fg(key_color),
|
||||
SpanStatic::from(" Enter setup "),
|
||||
SpanStatic::from("<UP/DOWN>").bold().fg(key_color),
|
||||
SpanStatic::from(" Change field "),
|
||||
SpanStatic::from("<TAB>").bold().fg(key_color),
|
||||
SpanStatic::from(" Switch tab "),
|
||||
SpanStatic::from("<BACKTAB>").bold().fg(key_color),
|
||||
SpanStatic::from(" Switch form "),
|
||||
SpanStatic::from("<ENTER>").bold().fg(key_color),
|
||||
SpanStatic::from(" Submit form "),
|
||||
SpanStatic::from("<F10|ESC>").bold().fg(key_color),
|
||||
SpanStatic::from(" Quit "),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for HelpFooter {
|
||||
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for HelpFooter {
|
||||
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
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
|
||||
//!
|
||||
//! `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
|
||||
mod actions;
|
||||
@@ -20,13 +20,16 @@ use std::time::Duration;
|
||||
// Includes
|
||||
use chrono::{DateTime, Local};
|
||||
use lib::browser;
|
||||
use lib::browser::Browser;
|
||||
use lib::browser::{Browser, FileExplorerTab};
|
||||
use lib::pane::Pane;
|
||||
use lib::transfer::{TransferOpts, TransferStates};
|
||||
use lib::walkdir::WalkdirStates;
|
||||
use remotefs::RemoteFs;
|
||||
use session::TransferPayload;
|
||||
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 crate::config::themes::Theme;
|
||||
@@ -34,7 +37,7 @@ use crate::explorer::{FileExplorer, FileSorting};
|
||||
use crate::filetransfer::{
|
||||
FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder,
|
||||
};
|
||||
use crate::host::HostBridge;
|
||||
use crate::host::RemoteBridged;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::watcher::FsWatcher;
|
||||
|
||||
@@ -67,8 +70,7 @@ enum Id {
|
||||
MkdirPopup,
|
||||
NewfilePopup,
|
||||
OpenWithPopup,
|
||||
ProgressBarFull,
|
||||
ProgressBarPartial,
|
||||
TransferProgressBar,
|
||||
QuitPopup,
|
||||
RenamePopup,
|
||||
ReplacePopup,
|
||||
@@ -237,10 +239,6 @@ pub struct FileTransferActivity {
|
||||
app: Application<Id, Msg, NoUserEvent>,
|
||||
/// Whether should redraw UI
|
||||
redraw: bool,
|
||||
/// Host bridge
|
||||
host_bridge: Box<dyn HostBridge>,
|
||||
/// Remote host client
|
||||
client: Box<dyn RemoteFs>,
|
||||
/// Browser
|
||||
browser: Browser,
|
||||
/// Current log lines
|
||||
@@ -253,10 +251,6 @@ pub struct FileTransferActivity {
|
||||
cache: Option<TempDir>,
|
||||
/// Fs watcher
|
||||
fswatcher: Option<FsWatcher>,
|
||||
/// host bridge connected
|
||||
host_bridge_connected: bool,
|
||||
/// remote connected once
|
||||
remote_connected: bool,
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
@@ -272,23 +266,34 @@ impl FileTransferActivity {
|
||||
let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client)?;
|
||||
let host_bridge_connected = host_bridge.is_localhost();
|
||||
let enable_fs_watcher = host_bridge.is_localhost();
|
||||
// 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 {
|
||||
exit_reason: None,
|
||||
context: None,
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.poll_timeout(ticks)
|
||||
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
||||
EventListenerCfg::default().crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
||||
),
|
||||
redraw: true,
|
||||
host_bridge,
|
||||
client: RemoteFsBuilder::build(
|
||||
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
|
||||
browser: Browser::new(local_pane, remote_pane),
|
||||
log_records: VecDeque::with_capacity(256),
|
||||
walkdir: WalkdirStates::default(),
|
||||
transfer: TransferStates::default(),
|
||||
cache: TempDir::new().ok(),
|
||||
@@ -297,11 +302,18 @@ impl FileTransferActivity {
|
||||
} else {
|
||||
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 {
|
||||
self.browser.host_bridge()
|
||||
}
|
||||
@@ -433,7 +445,7 @@ impl Activity for FileTransferActivity {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Get files at current pwd
|
||||
if self.host_bridge.is_localhost() {
|
||||
if self.browser.local_pane().fs.is_localhost() {
|
||||
debug!("Reloading host bridge directory");
|
||||
self.reload_host_bridge_dir();
|
||||
}
|
||||
@@ -460,14 +472,16 @@ impl Activity for FileTransferActivity {
|
||||
return;
|
||||
}
|
||||
// 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.host_bridge.is_localhost()
|
||||
&& !self.browser.local_pane().fs.is_localhost()
|
||||
{
|
||||
let host_bridge_params = self.context().host_bridge_params().unwrap();
|
||||
let ft_params = host_bridge_params.unwrap_protocol_params();
|
||||
// print params
|
||||
let msg: String = Self::get_connection_msg(ft_params);
|
||||
let msg: String = match host_bridge_params.protocol_params() {
|
||||
Some(ft_params) => Self::get_connection_msg(ft_params),
|
||||
None => String::from("Connecting..."),
|
||||
};
|
||||
// Set init state to connecting popup
|
||||
self.mount_blocking_wait(msg.as_str());
|
||||
// Connect to remote
|
||||
@@ -476,9 +490,10 @@ impl Activity for FileTransferActivity {
|
||||
self.redraw = true;
|
||||
}
|
||||
// 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.host_bridge.is_connected()
|
||||
&& self.browser.local_pane_mut().fs.is_connected()
|
||||
{
|
||||
let ftparams = self.context().remote_params().unwrap();
|
||||
// print params
|
||||
@@ -522,14 +537,21 @@ impl Activity for FileTransferActivity {
|
||||
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
||||
error!("Failed to clear screen: {}", err);
|
||||
}
|
||||
// Disconnect client
|
||||
if self.client.is_connected() {
|
||||
let _ = self.client.disconnect();
|
||||
// Disconnect remote
|
||||
if self.browser.remote_pane_mut().fs.is_connected() {
|
||||
let _ = self.browser.remote_pane_mut().fs.disconnect();
|
||||
}
|
||||
// disconnect host bridge
|
||||
if self.host_bridge.is_connected() {
|
||||
let _ = self.host_bridge.disconnect();
|
||||
// Disconnect host bridge
|
||||
if self.browser.local_pane_mut().fs.is_connected() {
|
||||
let _ = self.browser.local_pane_mut().fs.disconnect();
|
||||
}
|
||||
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
|
||||
//!
|
||||
//! `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 remotefs::File;
|
||||
use remotefs::fs::UnixPex;
|
||||
use tuirealm::{State, StateValue};
|
||||
use tuirealm::state::{State, StateValue};
|
||||
|
||||
use super::browser::FileExplorerTab;
|
||||
use super::lib::browser::FoundExplorerTab;
|
||||
@@ -92,43 +92,50 @@ impl FileTransferActivity {
|
||||
self.get_selected_files(&Id::ExplorerHostBridge)
|
||||
}
|
||||
|
||||
pub(crate) fn get_local_selected_file(&self) -> Option<File> {
|
||||
self.get_selected_file(&Id::ExplorerHostBridge)
|
||||
}
|
||||
|
||||
/// Get remote file entry
|
||||
pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile {
|
||||
self.get_selected_files(&Id::ExplorerRemote)
|
||||
}
|
||||
|
||||
pub(crate) fn get_remote_selected_file(&self) -> Option<File> {
|
||||
self.get_selected_file(&Id::ExplorerRemote)
|
||||
}
|
||||
|
||||
/// Returns whether only one entry is selected on local host
|
||||
pub(crate) fn is_local_selected_one(&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
|
||||
pub(crate) fn get_found_selected_entries(&mut self) -> SelectedFile {
|
||||
self.get_selected_files(&Id::ExplorerFind)
|
||||
}
|
||||
|
||||
pub(crate) fn get_found_selected_file(&self) -> Option<File> {
|
||||
self.get_selected_file(&Id::ExplorerFind)
|
||||
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
|
||||
|
||||
fn get_selected_index(&self, id: &Id) -> SelectedFileIndex {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -164,13 +171,13 @@ impl FileTransferActivity {
|
||||
|
||||
fn get_file_from_path(&mut self, id: &Id, path: &Path) -> Option<File> {
|
||||
match *id {
|
||||
Id::ExplorerHostBridge => self.host_bridge.stat(path).ok(),
|
||||
Id::ExplorerRemote => self.client.stat(path).ok(),
|
||||
Id::ExplorerHostBridge => self.browser.local_pane_mut().fs.stat(path).ok(),
|
||||
Id::ExplorerRemote => self.browser.remote_pane_mut().fs.stat(path).ok(),
|
||||
Id::ExplorerFind => {
|
||||
let found = self.browser.found_tab().unwrap();
|
||||
match found {
|
||||
FoundExplorerTab::Local => self.host_bridge.stat(path).ok(),
|
||||
FoundExplorerTab::Remote => self.client.stat(path).ok(),
|
||||
FoundExplorerTab::Local => self.browser.local_pane_mut().fs.stat(path).ok(),
|
||||
FoundExplorerTab::Remote => self.browser.remote_pane_mut().fs.stat(path).ok(),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
@@ -182,11 +189,16 @@ impl FileTransferActivity {
|
||||
Id::ExplorerHostBridge => self.host_bridge(),
|
||||
Id::ExplorerRemote => self.remote(),
|
||||
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);
|
||||
// if no transfer queue, return selected files
|
||||
match self.get_selected_index(id) {
|
||||
@@ -1,63 +1,45 @@
|
||||
//! ## 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
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use remotefs::File;
|
||||
|
||||
use super::{FileExplorerTab, FileTransferActivity, LogLevel, Msg, PendingActionMsg};
|
||||
use super::{FileTransferActivity, LogLevel, Msg, PendingActionMsg};
|
||||
|
||||
/// Describes destination for sync browsing
|
||||
enum SyncBrowsingDestination {
|
||||
pub(crate) enum SyncBrowsingDestination {
|
||||
Path(String),
|
||||
ParentDir,
|
||||
PreviousDir,
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Enter a directory on local host from entry
|
||||
pub(crate) fn action_enter_local_dir(&mut self, dir: File) {
|
||||
self.host_bridge_changedir(dir.path(), true);
|
||||
/// Enter a directory from entry, dispatching via the active tab's pane.
|
||||
pub(crate) fn action_enter_dir(&mut self, dir: File) {
|
||||
self.pane_changedir(dir.path(), true);
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Enter a directory on local host from entry
|
||||
pub(crate) fn action_enter_remote_dir(&mut self, dir: File) {
|
||||
self.remote_changedir(dir.path(), true);
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
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);
|
||||
/// Change directory reading value from input, dispatching via the active tab's pane.
|
||||
pub(crate) fn action_change_dir(&mut self, input: String) {
|
||||
let dir_path = self.pane_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.pane_changedir(dir_path.as_path(), true);
|
||||
// Check whether to sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
||||
}
|
||||
}
|
||||
|
||||
/// Change remote directory reading value from input
|
||||
pub(crate) fn action_change_remote_dir(&mut self, input: String) {
|
||||
let dir_path: PathBuf = self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.remote_changedir(dir_path.as_path(), true);
|
||||
// Check whether to sync
|
||||
if self.browser.sync_browsing && 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);
|
||||
/// Go to previous directory, dispatching via the active tab's pane.
|
||||
pub(crate) fn action_go_to_previous_dir(&mut self) {
|
||||
let prev = self.browser.fs_pane_mut().explorer.popd();
|
||||
if let Some(d) = prev {
|
||||
self.pane_changedir(d.as_path(), false);
|
||||
// Check whether to sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
||||
@@ -65,41 +47,12 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Go to previous directory from remote host
|
||||
pub(crate) fn action_go_to_previous_remote_dir(&mut self) {
|
||||
if let Some(d) = self.remote_mut().popd() {
|
||||
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
|
||||
/// Go to upper directory, dispatching via the active tab's pane.
|
||||
pub(crate) fn action_go_to_upper_dir(&mut self) {
|
||||
let path = self.browser.fs_pane().explorer.wrkdir.clone();
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.host_bridge_changedir(parent, true);
|
||||
// If sync is enabled update remote 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
|
||||
self.pane_changedir(parent, true);
|
||||
// If sync is enabled update the other side too
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
||||
}
|
||||
@@ -110,16 +63,18 @@ impl FileTransferActivity {
|
||||
|
||||
/// Synchronize browsing on the target browser.
|
||||
/// 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
|
||||
let path = match self.resolve_sync_browsing_destination(&destination) {
|
||||
Some(p) => p,
|
||||
None => return,
|
||||
};
|
||||
trace!("Synchronizing browsing to path {}", path.display());
|
||||
// Check whether destination exists on host
|
||||
let exists = match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => match self.client.exists(path.as_path()) {
|
||||
// Check whether destination exists on the opposite side
|
||||
let is_local = self.is_local_tab();
|
||||
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,
|
||||
Err(err) => {
|
||||
error!(
|
||||
@@ -129,8 +84,10 @@ impl FileTransferActivity {
|
||||
);
|
||||
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,
|
||||
Err(err) => {
|
||||
error!(
|
||||
@@ -140,8 +97,7 @@ impl FileTransferActivity {
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
_ => return,
|
||||
}
|
||||
};
|
||||
let name = path
|
||||
.file_name()
|
||||
@@ -159,12 +115,8 @@ impl FileTransferActivity {
|
||||
]) == Msg::PendingAction(PendingActionMsg::MakePendingDirectory)
|
||||
{
|
||||
trace!("User wants to create the unexisting directory");
|
||||
// Make directory
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_remote_mkdir(name.clone()),
|
||||
FileExplorerTab::Remote => self.action_local_mkdir(name.clone()),
|
||||
_ => {}
|
||||
}
|
||||
// Make directory on the opposite side
|
||||
self.sync_mkdir_on_opposite(name.clone());
|
||||
} else {
|
||||
// Do not synchronize, disable sync browsing and return
|
||||
trace!(
|
||||
@@ -183,23 +135,21 @@ impl FileTransferActivity {
|
||||
self.umount_sync_browsing_mkdir_popup();
|
||||
}
|
||||
trace!("Entering on the other explorer directory {}", name);
|
||||
// Enter directory
|
||||
match destination {
|
||||
SyncBrowsingDestination::ParentDir => match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
||||
_ => {}
|
||||
},
|
||||
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),
|
||||
_ => {}
|
||||
},
|
||||
SyncBrowsingDestination::PreviousDir => match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), false),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), false),
|
||||
_ => {}
|
||||
},
|
||||
// Enter directory on the opposite side
|
||||
let push = !matches!(destination, SyncBrowsingDestination::PreviousDir);
|
||||
if is_local {
|
||||
self.remote_changedir(path.as_path(), push);
|
||||
} else {
|
||||
self.local_changedir(path.as_path(), push);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a directory on the opposite side for sync browsing.
|
||||
fn sync_mkdir_on_opposite(&mut self, name: String) {
|
||||
if self.is_local_tab() {
|
||||
self.action_remote_mkdir(name);
|
||||
} else {
|
||||
self.action_local_mkdir(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,35 +158,34 @@ impl FileTransferActivity {
|
||||
&mut self,
|
||||
destination: &SyncBrowsingDestination,
|
||||
) -> Option<PathBuf> {
|
||||
match (destination, self.browser.tab()) {
|
||||
// NOTE: tab and methods are switched on purpose
|
||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::HostBridge) => {
|
||||
self.remote().wrkdir.parent().map(|x| x.to_path_buf())
|
||||
}
|
||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => {
|
||||
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)
|
||||
let is_local = self.is_local_tab();
|
||||
match destination {
|
||||
// NOTE: tab and methods are switched on purpose (we resolve from the opposite side)
|
||||
SyncBrowsingDestination::ParentDir => {
|
||||
if is_local {
|
||||
self.remote().wrkdir.parent().map(Path::to_path_buf)
|
||||
} else {
|
||||
warn!("Cannot synchronize browsing: remote has no previous directory in stack");
|
||||
None
|
||||
self.host_bridge().wrkdir.parent().map(Path::to_path_buf)
|
||||
}
|
||||
}
|
||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => {
|
||||
if let Some(p) = self.host_bridge_mut().popd() {
|
||||
SyncBrowsingDestination::PreviousDir => {
|
||||
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)
|
||||
} else {
|
||||
warn!("Cannot synchronize browsing: local has no previous directory in stack");
|
||||
None
|
||||
}
|
||||
}
|
||||
(SyncBrowsingDestination::Path(p), _) => Some(PathBuf::from(p.as_str())),
|
||||
_ => {
|
||||
warn!("Cannot synchronize browsing for current explorer");
|
||||
None
|
||||
}
|
||||
SyncBrowsingDestination::Path(p) => Some(PathBuf::from(p.as_str())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,83 +3,12 @@ use remotefs::fs::UnixPex;
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub fn action_local_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_local_selected_entries().get_files();
|
||||
/// Change file mode for the currently selected entries via the active tab's pane.
|
||||
pub(crate) fn action_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_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_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) {
|
||||
if let Err(err) = self.browser.fs_pane_mut().fs.chmod(file.path(), mode) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
|
||||
@@ -1,61 +1,38 @@
|
||||
//! ## 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
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use remotefs::{File, RemoteErrorType};
|
||||
use remotefs::File;
|
||||
|
||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Copy file on local
|
||||
pub(crate) fn action_local_copy(&mut self, input: String) {
|
||||
match self.get_local_selected_entries() {
|
||||
/// Copy the currently selected file(s) via the active tab's pane.
|
||||
pub(crate) fn action_copy(&mut self, input: String) {
|
||||
match self.get_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
let dest_path: PathBuf = PathBuf::from(input);
|
||||
self.local_copy_file(&entry, dest_path.as_path());
|
||||
let dest_path = PathBuf::from(input);
|
||||
self.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.local_copy_file(&entry, dest_path.as_path());
|
||||
self.copy_file(entry, dest_path.as_path());
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
// clear selection and reload
|
||||
self.browser.explorer_mut().clear_queue();
|
||||
self.reload_browser_file_list();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy file on remote
|
||||
pub(crate) fn action_remote_copy(&mut self, input: String) {
|
||||
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) {
|
||||
fn copy_file(&mut self, entry: File, dest: &Path) {
|
||||
match self.browser.fs_pane_mut().fs.copy(&entry, dest) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
@@ -66,45 +43,23 @@ impl FileTransferActivity {
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not copy \"{}\" to \"{}\": {}",
|
||||
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
|
||||
Err(err) => {
|
||||
// On remote tabs, fall back to tricky_copy (download + re-upload)
|
||||
// when the protocol doesn't support server-side copy.
|
||||
if !self.is_local_tab() {
|
||||
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);
|
||||
}
|
||||
// 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,
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
@@ -191,9 +151,17 @@ impl FileTransferActivity {
|
||||
return Err(err);
|
||||
}
|
||||
// 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(_) => 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) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## 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
|
||||
use remotefs::File;
|
||||
@@ -8,72 +8,28 @@ use remotefs::File;
|
||||
use super::{FileTransferActivity, LogLevel, SelectedFile};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_delete(&mut self) {
|
||||
match self.get_local_selected_entries() {
|
||||
/// Delete the currently selected file(s) via the active tab's pane.
|
||||
pub(crate) fn action_delete(&mut self) {
|
||||
match self.get_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
// Delete file
|
||||
self.local_remove_file(&entry);
|
||||
self.remove_file(&entry);
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for (entry, _) in entries.iter() {
|
||||
// Delete file
|
||||
self.local_remove_file(entry);
|
||||
self.remove_file(entry);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
// clear selection and reload
|
||||
self.browser.explorer_mut().clear_queue();
|
||||
self.reload_browser_file_list();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_remote_delete(&mut self) {
|
||||
match self.get_remote_selected_entries() {
|
||||
SelectedFile::One(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()) {
|
||||
/// Remove a single file or directory via the active tab's pane.
|
||||
pub(crate) fn remove_file(&mut self, entry: &File) {
|
||||
match self.browser.fs_pane_mut().fs.remove(entry) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## 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::io::Read;
|
||||
@@ -9,7 +9,9 @@ use std::time::SystemTime;
|
||||
|
||||
use remotefs::File;
|
||||
use remotefs::fs::Metadata;
|
||||
use tuirealm::terminal::TerminalAdapter;
|
||||
|
||||
use super::super::ui_result;
|
||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||
|
||||
impl FileTransferActivity {
|
||||
@@ -28,7 +30,7 @@ impl FileTransferActivity {
|
||||
format!("Opening file \"{}\"…", entry.path().display()),
|
||||
);
|
||||
// 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(|_| ()),
|
||||
false => self.edit_bridged_local_file(entry),
|
||||
};
|
||||
@@ -88,7 +90,7 @@ impl FileTransferActivity {
|
||||
};
|
||||
|
||||
// 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,
|
||||
Err(err) => {
|
||||
return Err(format!("Failed to open bridged entry: {err}"));
|
||||
@@ -122,7 +124,7 @@ impl FileTransferActivity {
|
||||
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(),
|
||||
&Metadata {
|
||||
size: new_file_size,
|
||||
@@ -139,7 +141,9 @@ impl FileTransferActivity {
|
||||
return Err(format!("Could not write file: {err}"));
|
||||
}
|
||||
|
||||
self.host_bridge
|
||||
self.browser
|
||||
.local_pane_mut()
|
||||
.fs
|
||||
.finalize_write(writer)
|
||||
.map_err(|err| format!("Could not write file: {err}"))?;
|
||||
}
|
||||
@@ -177,7 +181,7 @@ impl FileTransferActivity {
|
||||
error!("Could not leave alternate screen: {}", err);
|
||||
}
|
||||
// Lock ports
|
||||
assert!(self.app.lock_ports().is_ok());
|
||||
ui_result(self.app.lock_ports());
|
||||
// Get current file modification time
|
||||
let prev_mtime = self.get_localhost_mtime(path)?;
|
||||
// Open editor
|
||||
@@ -205,7 +209,7 @@ impl FileTransferActivity {
|
||||
error!("Could not clear screen screen: {}", err);
|
||||
}
|
||||
// Unlock ports
|
||||
assert!(self.app.unlock_ports().is_ok());
|
||||
ui_result(self.app.unlock_ports());
|
||||
}
|
||||
let after_mtime = self.get_localhost_mtime(path)?;
|
||||
|
||||
@@ -241,7 +245,8 @@ impl FileTransferActivity {
|
||||
return Err(format!("Could not open file {file_name}: {err}"));
|
||||
}
|
||||
// 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),
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
@@ -254,7 +259,7 @@ impl FileTransferActivity {
|
||||
// Edit file
|
||||
self.edit_local_file(tmpfile.as_path())?;
|
||||
// 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,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
@@ -280,7 +285,7 @@ impl FileTransferActivity {
|
||||
),
|
||||
);
|
||||
// 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,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## 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::str::FromStr;
|
||||
@@ -29,25 +29,22 @@ impl FromStr for Command {
|
||||
Err("cd command requires a path".to_string())
|
||||
}
|
||||
}
|
||||
Some("exit") | Some("logout") => Ok(Command::Exit),
|
||||
Some(cmd) => Ok(Command::Exec(cmd.to_string())),
|
||||
None => Err("".to_string()),
|
||||
Some("exit" | "logout") => Ok(Command::Exit),
|
||||
Some(_) => Ok(Command::Exec(s.to_string())),
|
||||
None => Err(String::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_exec(&mut self, input: String) {
|
||||
self.action_exec(false, input);
|
||||
pub(crate) fn action_exec_cmd(&mut self, input: String) {
|
||||
self.action_exec(input);
|
||||
}
|
||||
|
||||
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
||||
self.action_exec(true, input);
|
||||
}
|
||||
|
||||
fn action_exec(&mut self, remote: bool, cmd: String) {
|
||||
fn action_exec(&mut self, cmd: String) {
|
||||
if cmd.is_empty() {
|
||||
self.print_terminal("".to_string());
|
||||
self.print_terminal(String::new());
|
||||
return;
|
||||
}
|
||||
|
||||
let cmd = match Command::from_str(&cmd) {
|
||||
@@ -61,10 +58,10 @@ impl FileTransferActivity {
|
||||
|
||||
match cmd {
|
||||
Command::Cd(path) => {
|
||||
self.action_exec_cd(remote, path);
|
||||
self.action_exec_cd(path);
|
||||
}
|
||||
Command::Exec(executable) => {
|
||||
self.action_exec_executable(remote, executable);
|
||||
self.action_exec_executable(executable);
|
||||
}
|
||||
Command::Exit => {
|
||||
self.action_exec_exit();
|
||||
@@ -77,43 +74,20 @@ impl FileTransferActivity {
|
||||
self.umount_exec();
|
||||
}
|
||||
|
||||
fn action_exec_cd(&mut self, remote: bool, input: String) {
|
||||
let new_dir = if remote {
|
||||
let dir_path: PathBuf =
|
||||
self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.remote_changedir(dir_path.as_path(), true);
|
||||
|
||||
dir_path
|
||||
} else {
|
||||
let dir_path: PathBuf =
|
||||
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.host_bridge_changedir(dir_path.as_path(), true);
|
||||
|
||||
dir_path
|
||||
};
|
||||
fn action_exec_cd(&mut self, input: String) {
|
||||
let dir_path = self.pane_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.pane_changedir(dir_path.as_path(), true);
|
||||
|
||||
self.update_browser_file_list();
|
||||
|
||||
// update prompt and print the new directory
|
||||
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
|
||||
fn action_exec_executable(&mut self, remote: bool, cmd: String) {
|
||||
let res = if remote {
|
||||
self.client
|
||||
.as_mut()
|
||||
.exec(cmd.as_str())
|
||||
.map(|(_, output)| output)
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
self.host_bridge
|
||||
.exec(cmd.as_str())
|
||||
.map_err(|e| e.to_string())
|
||||
};
|
||||
|
||||
match res {
|
||||
/// Execute a command via the active tab's pane.
|
||||
fn action_exec_executable(&mut self, cmd: String) {
|
||||
match self.browser.fs_pane_mut().fs.exec(cmd.as_str()) {
|
||||
Ok(output) => {
|
||||
self.print_terminal(output);
|
||||
}
|
||||
@@ -122,7 +96,7 @@ impl FileTransferActivity {
|
||||
LogLevel::Error,
|
||||
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 super::{FileTransferActivity, LogLevel};
|
||||
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum Host {
|
||||
HostBridge,
|
||||
Remote,
|
||||
}
|
||||
|
||||
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) {
|
||||
// Get selected file
|
||||
self.mount_blocking_wait("Getting total path size...");
|
||||
|
||||
let total_size = match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => {
|
||||
let files = self.get_local_selected_entries().get_files();
|
||||
self.get_files_size(files, Host::HostBridge)
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
let files = self.get_remote_selected_entries().get_files();
|
||||
self.get_files_size(files, Host::Remote)
|
||||
}
|
||||
FileExplorerTab::FindHostBridge => {
|
||||
let files = self.get_found_selected_entries().get_files();
|
||||
self.get_files_size(files, Host::HostBridge)
|
||||
}
|
||||
FileExplorerTab::FindRemote => {
|
||||
let files = self.get_found_selected_entries().get_files();
|
||||
self.get_files_size(files, Host::Remote)
|
||||
}
|
||||
};
|
||||
let files = self.get_selected_entries().get_files();
|
||||
let total_size = self.get_files_size(files);
|
||||
|
||||
self.umount_wait();
|
||||
self.mount_info(format!(
|
||||
@@ -40,24 +17,19 @@ impl FileTransferActivity {
|
||||
));
|
||||
}
|
||||
|
||||
fn get_files_size(&mut self, files: Vec<File>, host: Host) -> u64 {
|
||||
files.into_iter().map(|f| self.get_file_size(f, host)).sum()
|
||||
fn get_files_size(&mut self, files: Vec<File>) -> u64 {
|
||||
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 {
|
||||
// stat
|
||||
let stat_res = match host {
|
||||
Host::HostBridge => self.host_bridge.stat(symlink).map_err(|e| e.to_string()),
|
||||
Host::Remote => self.client.stat(symlink).map_err(|e| e.to_string()),
|
||||
};
|
||||
match stat_res {
|
||||
match self.browser.fs_pane_mut().fs.stat(symlink) {
|
||||
Ok(stat) => stat.metadata().size,
|
||||
Err(err_msg) => {
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Failed to stat symlink target {path}: {err_msg}",
|
||||
"Failed to stat symlink target {path}: {err}",
|
||||
path = symlink.display(),
|
||||
),
|
||||
);
|
||||
@@ -65,22 +37,13 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
} else if file.is_dir() {
|
||||
// list and sum
|
||||
let list_res = match host {
|
||||
Host::HostBridge => self
|
||||
.host_bridge
|
||||
.list_dir(&file.path)
|
||||
.map_err(|e| e.to_string()),
|
||||
Host::Remote => self.client.list_dir(&file.path).map_err(|e| e.to_string()),
|
||||
};
|
||||
|
||||
match list_res {
|
||||
Ok(list) => list.into_iter().map(|f| self.get_file_size(f, host)).sum(),
|
||||
Err(err_msg) => {
|
||||
match self.browser.fs_pane_mut().fs.list_dir(&file.path) {
|
||||
Ok(list) => list.into_iter().map(|f| self.get_file_size(f)).sum(),
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Failed to list directory {path}: {err_msg}",
|
||||
"Failed to list directory {path}: {err}",
|
||||
path = file.path.display(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! ## 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
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::super::browser::FileExplorerTab;
|
||||
use super::change_dir::SyncBrowsingDestination;
|
||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
||||
|
||||
impl FileTransferActivity {
|
||||
@@ -22,33 +22,27 @@ impl FileTransferActivity {
|
||||
Some(p) => p.to_path_buf(),
|
||||
}
|
||||
};
|
||||
// Change directory
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.host_bridge_changedir(path.as_path(), true)
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
self.remote_changedir(path.as_path(), true)
|
||||
}
|
||||
// Change directory on the active tab's pane
|
||||
self.pane_changedir(path.as_path(), true);
|
||||
// Sync browsing: mirror the directory change on the opposite pane
|
||||
if self.browser.sync_browsing {
|
||||
let dir_name = path
|
||||
.file_name()
|
||||
.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) {
|
||||
let wrkdir: PathBuf = match self.browser.tab() {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.remote().wrkdir.clone()
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
self.host_bridge().wrkdir.clone()
|
||||
}
|
||||
};
|
||||
let wrkdir: PathBuf = self.browser.opposite_pane().explorer.wrkdir.clone();
|
||||
match self.get_found_selected_entries() {
|
||||
SelectedFile::One(entry) => match self.browser.tab() {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
SelectedFile::One(entry) => {
|
||||
if self.is_local_tab() {
|
||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||
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(
|
||||
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
||||
)
|
||||
@@ -66,11 +60,10 @@ impl FileTransferActivity {
|
||||
format!("Could not upload file: {err}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
} else {
|
||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||
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(
|
||||
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
||||
)
|
||||
@@ -89,7 +82,7 @@ impl FileTransferActivity {
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// In case of selection: save multiple files in wrkdir/input
|
||||
let mut dest_path: PathBuf = wrkdir;
|
||||
@@ -97,58 +90,51 @@ impl FileTransferActivity {
|
||||
dest_path.push(save_as);
|
||||
}
|
||||
// Iter files
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
||||
entries,
|
||||
) = self.get_files_to_transfer_with_overwrites(
|
||||
if self.is_local_tab() {
|
||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
|
||||
self.get_files_to_transfer_with_overwrites(
|
||||
entries,
|
||||
super::save::CheckFileExists::Remote,
|
||||
)
|
||||
else {
|
||||
debug!("User cancelled file transfer due to overwrites");
|
||||
return;
|
||||
};
|
||||
if let Err(err) = self.filetransfer_send(
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
{
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not upload file: {err}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
debug!("User cancelled file transfer due to overwrites");
|
||||
return;
|
||||
};
|
||||
if let Err(err) = self.filetransfer_send(
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not upload file: {err}"),
|
||||
);
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
||||
entries,
|
||||
) = self.get_files_to_transfer_with_overwrites(
|
||||
} else {
|
||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
|
||||
self.get_files_to_transfer_with_overwrites(
|
||||
entries,
|
||||
super::save::CheckFileExists::HostBridge,
|
||||
)
|
||||
else {
|
||||
debug!("User cancelled file transfer due to overwrites");
|
||||
return;
|
||||
};
|
||||
if let Err(err) = self.filetransfer_recv(
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not download file: {err}"),
|
||||
);
|
||||
}
|
||||
else {
|
||||
debug!("User cancelled file transfer due to overwrites");
|
||||
return;
|
||||
};
|
||||
if let Err(err) = self.filetransfer_recv(
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not download file: {err}"),
|
||||
);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
if let Some(f) = self.found_mut() {
|
||||
f.clear_queue();
|
||||
self.update_find_list();
|
||||
}
|
||||
// clear selection
|
||||
if let Some(f) = self.found_mut() {
|
||||
f.clear_queue();
|
||||
self.update_find_list();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,14 +145,11 @@ impl FileTransferActivity {
|
||||
pub(crate) fn action_find_delete(&mut self) {
|
||||
match self.get_found_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
// Delete file
|
||||
self.remove_found_file(&entry);
|
||||
self.remove_file(&entry);
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for (entry, _) in entries.iter() {
|
||||
// Delete file
|
||||
self.remove_found_file(entry);
|
||||
self.remove_file(entry);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
match self.get_found_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
@@ -235,13 +207,6 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
fn open_found_file(&mut self, entry: &File, with: Option<&str>) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.action_open_local_file(entry, with);
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
self.action_open_remote_file(entry, with);
|
||||
}
|
||||
}
|
||||
self.open_file(entry, with);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## 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;
|
||||
|
||||
|
||||
@@ -1,49 +1,46 @@
|
||||
//! ## 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
|
||||
use std::path::PathBuf;
|
||||
|
||||
use remotefs::fs::UnixPex;
|
||||
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_mkdir(&mut self, input: String) {
|
||||
match self
|
||||
.host_bridge
|
||||
.mkdir(PathBuf::from(input.as_str()).as_path())
|
||||
{
|
||||
Ok(_) => {
|
||||
// Reload files
|
||||
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
||||
}
|
||||
Err(err) => {
|
||||
// Report err
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create directory \"{input}\": {err}"),
|
||||
);
|
||||
}
|
||||
/// Create a directory via the active tab's pane.
|
||||
pub(crate) fn action_mkdir(&mut self, input: String) {
|
||||
let path = PathBuf::from(input.as_str());
|
||||
match self.browser.fs_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}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
pub(crate) fn action_remote_mkdir(&mut self, input: String) {
|
||||
match self.client.as_mut().create_dir(
|
||||
PathBuf::from(input.as_str()).as_path(),
|
||||
UnixPex::from(0o755),
|
||||
) {
|
||||
Ok(_) => {
|
||||
// Reload files
|
||||
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
||||
}
|
||||
Err(err) => {
|
||||
// Report err
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create directory \"{input}\": {err}"),
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a directory on the local host (used by sync-browsing in change_dir).
|
||||
pub(in crate::ui::activities::filetransfer) fn action_local_mkdir(&mut self, input: String) {
|
||||
let path = PathBuf::from(input.as_str());
|
||||
match self.browser.local_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}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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