mirror of
https://github.com/veeso/termscp.git
synced 2026-09-25 21:41:20 -07:00
refactor: 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.
This commit is contained in:
@@ -24,3 +24,7 @@ dist/pkgs/arch/*.tar.gz
|
|||||||
|
|
||||||
dist/pkgs/
|
dist/pkgs/
|
||||||
dist/build/macos/openssl/
|
dist/build/macos/openssl/
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# 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 put plans to `./.claude/plans/`
|
||||||
Generated
+734
-469
File diff suppressed because it is too large
Load Diff
+7
-7
@@ -55,7 +55,7 @@ notify = "8"
|
|||||||
notify-rust = { version = "^4", default-features = false, features = ["d"] }
|
notify-rust = { version = "^4", default-features = false, features = ["d"] }
|
||||||
nucleo = "0.5"
|
nucleo = "0.5"
|
||||||
open = "5"
|
open = "5"
|
||||||
rand = "^0.9"
|
rand = "0.10"
|
||||||
regex = "^1"
|
regex = "^1"
|
||||||
remotefs = "^0.3"
|
remotefs = "^0.3"
|
||||||
remotefs-aws-s3 = "0.4"
|
remotefs-aws-s3 = "0.4"
|
||||||
@@ -73,21 +73,21 @@ self_update = { version = "^0.42", default-features = false, features = [
|
|||||||
serde = { version = "^1", features = ["derive"] }
|
serde = { version = "^1", features = ["derive"] }
|
||||||
shellexpand = "3"
|
shellexpand = "3"
|
||||||
simplelog = "^0.12"
|
simplelog = "^0.12"
|
||||||
ssh2-config = "^0.6"
|
ssh2-config = "^0.7"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = ["rt"] }
|
tokio = { version = "1", features = ["rt"] }
|
||||||
toml = "^0.9"
|
toml = "1"
|
||||||
tui-realm-stdlib = "3"
|
tui-realm-stdlib = "3"
|
||||||
tuirealm = "3"
|
tuirealm = "3"
|
||||||
tui-term = "0.2"
|
tui-term = "0.2"
|
||||||
unicode-width = "0.2"
|
unicode-width = "0.2"
|
||||||
version-compare = "^0.2"
|
version-compare = "^0.2"
|
||||||
whoami = "^1.6"
|
whoami = "2"
|
||||||
wildmatch = "2"
|
wildmatch = "2"
|
||||||
|
|
||||||
[target."cfg(target_family = \"unix\")".dependencies]
|
[target."cfg(target_family = \"unix\")".dependencies]
|
||||||
remotefs-ftp = { version = "^0.3", features = [
|
remotefs-ftp = { version = "^0.4", features = [
|
||||||
"native-tls-vendored",
|
"native-tls-vendored",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
] }
|
] }
|
||||||
@@ -98,7 +98,7 @@ remotefs-ssh = { version = "^0.7", default-features = false, features = [
|
|||||||
uzers = "0.12"
|
uzers = "0.12"
|
||||||
|
|
||||||
[target."cfg(target_family = \"windows\")".dependencies]
|
[target."cfg(target_family = \"windows\")".dependencies]
|
||||||
remotefs-ftp = { version = "^0.3", features = ["native-tls"] }
|
remotefs-ftp = { version = "^0.4", features = ["native-tls"] }
|
||||||
remotefs-ssh = { version = "^0.7" }
|
remotefs-ssh = { version = "^0.7" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
@@ -107,7 +107,7 @@ serial_test = "^3"
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cfg_aliases = "0.2"
|
cfg_aliases = "0.2"
|
||||||
vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] }
|
vergen-git2 = { version = "9", features = ["build", "cargo", "rustc", "si"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["smb", "keyring"]
|
default = ["smb", "keyring"]
|
||||||
|
|||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
[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"
|
||||||
@@ -187,14 +187,15 @@ impl ActivityManager {
|
|||||||
// * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password
|
// * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password
|
||||||
let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config());
|
let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config());
|
||||||
let generic_params = params.generic_params().unwrap();
|
let generic_params = params.generic_params().unwrap();
|
||||||
|
let username = generic_params
|
||||||
|
.username
|
||||||
|
.clone()
|
||||||
|
.map(Ok)
|
||||||
|
.unwrap_or_else(whoami::username)
|
||||||
|
.map_err(|err| format!("Could not get current username: {err}"))?;
|
||||||
|
|
||||||
if storage
|
if storage
|
||||||
.resolve(
|
.resolve(&generic_params.address, &username)
|
||||||
&generic_params.address,
|
|
||||||
&generic_params
|
|
||||||
.username
|
|
||||||
.clone()
|
|
||||||
.unwrap_or(whoami::username()),
|
|
||||||
)
|
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
debug!(
|
debug!(
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ impl HostBridgeParams {
|
|||||||
/// Returns the host name for the bridge params
|
/// Returns the host name for the bridge params
|
||||||
pub fn username(&self) -> Option<String> {
|
pub fn username(&self) -> Option<String> {
|
||||||
match self {
|
match self {
|
||||||
HostBridgeParams::Localhost(_) => Some(whoami::username()),
|
HostBridgeParams::Localhost(_) => whoami::username().ok(),
|
||||||
HostBridgeParams::Remote(_, params) => {
|
HostBridgeParams::Remote(_, params) => {
|
||||||
params.generic_params().and_then(|p| p.username.clone())
|
params.generic_params().and_then(|p| p.username.clone())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,18 +173,16 @@ impl RemoteFsBuilder {
|
|||||||
credentials = credentials.workgroup(workgroup);
|
credentials = credentials.workgroup(workgroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
match SmbFs::try_new(
|
SmbFs::try_new(
|
||||||
credentials,
|
credentials,
|
||||||
SmbOptions::default()
|
SmbOptions::default()
|
||||||
.one_share_per_server(true)
|
.one_share_per_server(true)
|
||||||
.case_sensitive(false),
|
.case_sensitive(false),
|
||||||
) {
|
)
|
||||||
Ok(fs) => fs,
|
.unwrap_or_else(|e| {
|
||||||
Err(e) => {
|
error!("Invalid params for protocol SMB: {e}");
|
||||||
error!("Invalid params for protocol SMB: {e}");
|
panic!("Invalid params for protocol SMB: {e}")
|
||||||
panic!("Invalid params for protocol SMB: {e}")
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(smb_windows)]
|
#[cfg(smb_windows)]
|
||||||
@@ -236,12 +234,13 @@ impl RemoteFsBuilder {
|
|||||||
} else {
|
} else {
|
||||||
//* case 3: use system username; can't be None
|
//* case 3: use system username; can't be None
|
||||||
debug!("no username was provided, using current username");
|
debug!("no username was provided, using current username");
|
||||||
opts = opts.username(whoami::username());
|
if let Ok(username) = whoami::username() {
|
||||||
|
opts = opts.username(username);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if let Ok(username) = whoami::username() {
|
||||||
//* case 3: use system username; can't be None
|
|
||||||
debug!("no username was provided, using current username");
|
debug!("no username was provided, using current username");
|
||||||
opts = opts.username(whoami::username());
|
opts = opts.username(username);
|
||||||
}
|
}
|
||||||
// For SSH protocols, only set password if explicitly provided and non-empty.
|
// For SSH protocols, only set password if explicitly provided and non-empty.
|
||||||
// This allows the SSH library to prioritize key-based and agent authentication.
|
// This allows the SSH library to prioritize key-based and agent authentication.
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
//!
|
//!
|
||||||
//! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system
|
//! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system
|
||||||
|
|
||||||
// Crate
|
|
||||||
// Ext
|
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
@@ -12,11 +10,8 @@ use std::time::SystemTime;
|
|||||||
use super::keys::filestorage::FileStorage;
|
use super::keys::filestorage::FileStorage;
|
||||||
use super::keys::keyringstorage::KeyringStorage;
|
use super::keys::keyringstorage::KeyringStorage;
|
||||||
use super::keys::{KeyStorage, KeyStorageError};
|
use super::keys::{KeyStorage, KeyStorageError};
|
||||||
// Local
|
use crate::config::bookmarks::{Bookmark, UserHosts};
|
||||||
use crate::config::{
|
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
|
||||||
bookmarks::{Bookmark, UserHosts},
|
|
||||||
serialization::{SerializerError, SerializerErrorKind, deserialize, serialize},
|
|
||||||
};
|
|
||||||
use crate::filetransfer::FileTransferParams;
|
use crate::filetransfer::FileTransferParams;
|
||||||
use crate::utils::crypto;
|
use crate::utils::crypto;
|
||||||
use crate::utils::fmt::fmt_time;
|
use crate::utils::fmt::fmt_time;
|
||||||
@@ -104,7 +99,7 @@ impl BookmarksClient {
|
|||||||
fn keyring(storage_path: &Path, keyring: bool) -> (Box<dyn KeyStorage>, &'static str) {
|
fn keyring(storage_path: &Path, keyring: bool) -> (Box<dyn KeyStorage>, &'static str) {
|
||||||
if keyring && cfg!(feature = "keyring") {
|
if keyring && cfg!(feature = "keyring") {
|
||||||
debug!("Setting up KeyStorage");
|
debug!("Setting up KeyStorage");
|
||||||
let username: String = whoami::username();
|
let username = whoami::username().unwrap_or_default();
|
||||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||||
// Check if keyring storage is supported
|
// Check if keyring storage is supported
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
|
|||||||
@@ -78,14 +78,13 @@ impl KeyStorage for KeyringStorage {
|
|||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))]
|
#[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))]
|
||||||
fn test_system_keys_keyringstorage() {
|
fn test_system_keys_keyring_storage() {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use whoami::username;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
let username: String = username();
|
let username = whoami::username().expect("no username");
|
||||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
let storage = KeyringStorage::new(username.as_str());
|
||||||
assert!(storage.is_supported());
|
assert!(storage.is_supported());
|
||||||
let app_name: &str = "termscp-test2";
|
let app_name: &str = "termscp-test2";
|
||||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
|
|
||||||
use super::{FileExplorerTab, FileTransferActivity, LogLevel, Msg, PendingActionMsg};
|
use super::{FileTransferActivity, LogLevel, Msg, PendingActionMsg};
|
||||||
|
|
||||||
/// Describes destination for sync browsing
|
/// Describes destination for sync browsing
|
||||||
enum SyncBrowsingDestination {
|
enum SyncBrowsingDestination {
|
||||||
@@ -17,47 +17,29 @@ enum SyncBrowsingDestination {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// Enter a directory on local host from entry
|
/// Enter a directory from entry, dispatching via the active tab's pane.
|
||||||
pub(crate) fn action_enter_local_dir(&mut self, dir: File) {
|
pub(crate) fn action_enter_dir(&mut self, dir: File) {
|
||||||
self.host_bridge_changedir(dir.path(), true);
|
self.pane_changedir(dir.path(), true);
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enter a directory on local host from entry
|
/// Change directory reading value from input, dispatching via the active tab's pane.
|
||||||
pub(crate) fn action_enter_remote_dir(&mut self, dir: File) {
|
pub(crate) fn action_change_dir(&mut self, input: String) {
|
||||||
self.remote_changedir(dir.path(), true);
|
let dir_path = self.pane_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
self.pane_changedir(dir_path.as_path(), true);
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change local directory reading value from input
|
|
||||||
pub(crate) fn action_change_local_dir(&mut self, input: String) {
|
|
||||||
let dir_path: PathBuf =
|
|
||||||
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
|
||||||
self.host_bridge_changedir(dir_path.as_path(), true);
|
|
||||||
// Check whether to sync
|
// Check whether to sync
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change remote directory reading value from input
|
/// Go to previous directory, dispatching via the active tab's pane.
|
||||||
pub(crate) fn action_change_remote_dir(&mut self, input: String) {
|
pub(crate) fn action_go_to_previous_dir(&mut self) {
|
||||||
let dir_path: PathBuf = self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
let prev = self.browser.fs_pane_mut().explorer.popd();
|
||||||
self.remote_changedir(dir_path.as_path(), true);
|
if let Some(d) = prev {
|
||||||
// Check whether to sync
|
self.pane_changedir(d.as_path(), false);
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Go to previous directory from localhost
|
|
||||||
pub(crate) fn action_go_to_previous_local_dir(&mut self) {
|
|
||||||
if let Some(d) = self.host_bridge_mut().popd() {
|
|
||||||
self.host_bridge_changedir(d.as_path(), false);
|
|
||||||
// Check whether to sync
|
// Check whether to sync
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
||||||
@@ -65,41 +47,12 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Go to previous directory from remote host
|
/// Go to upper directory, dispatching via the active tab's pane.
|
||||||
pub(crate) fn action_go_to_previous_remote_dir(&mut self) {
|
pub(crate) fn action_go_to_upper_dir(&mut self) {
|
||||||
if let Some(d) = self.remote_mut().popd() {
|
let path = self.browser.fs_pane().explorer.wrkdir.clone();
|
||||||
self.remote_changedir(d.as_path(), false);
|
|
||||||
// Check whether to sync
|
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Go to upper directory on local host
|
|
||||||
pub(crate) fn action_go_to_local_upper_dir(&mut self) {
|
|
||||||
// Get pwd
|
|
||||||
let path: PathBuf = self.host_bridge().wrkdir.clone();
|
|
||||||
// Go to parent directory
|
|
||||||
if let Some(parent) = path.as_path().parent() {
|
if let Some(parent) = path.as_path().parent() {
|
||||||
self.host_bridge_changedir(parent, true);
|
self.pane_changedir(parent, true);
|
||||||
// If sync is enabled update remote too
|
// If sync is enabled update the other side too
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// #### action_go_to_remote_upper_dir
|
|
||||||
///
|
|
||||||
/// Go to upper directory on remote host
|
|
||||||
pub(crate) fn action_go_to_remote_upper_dir(&mut self) {
|
|
||||||
// Get pwd
|
|
||||||
let path: PathBuf = self.remote().wrkdir.clone();
|
|
||||||
// Go to parent directory
|
|
||||||
if let Some(parent) = path.as_path().parent() {
|
|
||||||
self.remote_changedir(parent, true);
|
|
||||||
// If sync is enabled update local too
|
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
||||||
}
|
}
|
||||||
@@ -117,9 +70,11 @@ impl FileTransferActivity {
|
|||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
trace!("Synchronizing browsing to path {}", path.display());
|
trace!("Synchronizing browsing to path {}", path.display());
|
||||||
// Check whether destination exists on host
|
// Check whether destination exists on the opposite side
|
||||||
let exists = match self.browser.tab() {
|
let is_local = self.is_local_tab();
|
||||||
FileExplorerTab::HostBridge => match self.client.exists(path.as_path()) {
|
let exists = if is_local {
|
||||||
|
// Current tab is local, so the opposite is remote
|
||||||
|
match self.browser.remote_pane_mut().fs.exists(path.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -129,8 +84,10 @@ impl FileTransferActivity {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
FileExplorerTab::Remote => match self.host_bridge.exists(path.as_path()) {
|
} else {
|
||||||
|
// Current tab is remote, so the opposite is local
|
||||||
|
match self.browser.local_pane_mut().fs.exists(path.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -140,8 +97,7 @@ impl FileTransferActivity {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => return,
|
|
||||||
};
|
};
|
||||||
let name = path
|
let name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -159,12 +115,8 @@ impl FileTransferActivity {
|
|||||||
]) == Msg::PendingAction(PendingActionMsg::MakePendingDirectory)
|
]) == Msg::PendingAction(PendingActionMsg::MakePendingDirectory)
|
||||||
{
|
{
|
||||||
trace!("User wants to create the unexisting directory");
|
trace!("User wants to create the unexisting directory");
|
||||||
// Make directory
|
// Make directory on the opposite side
|
||||||
match self.browser.tab() {
|
self.sync_mkdir_on_opposite(name.clone());
|
||||||
FileExplorerTab::HostBridge => self.action_remote_mkdir(name.clone()),
|
|
||||||
FileExplorerTab::Remote => self.action_local_mkdir(name.clone()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Do not synchronize, disable sync browsing and return
|
// Do not synchronize, disable sync browsing and return
|
||||||
trace!(
|
trace!(
|
||||||
@@ -183,23 +135,21 @@ impl FileTransferActivity {
|
|||||||
self.umount_sync_browsing_mkdir_popup();
|
self.umount_sync_browsing_mkdir_popup();
|
||||||
}
|
}
|
||||||
trace!("Entering on the other explorer directory {}", name);
|
trace!("Entering on the other explorer directory {}", name);
|
||||||
// Enter directory
|
// Enter directory on the opposite side
|
||||||
match destination {
|
let push = !matches!(destination, SyncBrowsingDestination::PreviousDir);
|
||||||
SyncBrowsingDestination::ParentDir => match self.browser.tab() {
|
if is_local {
|
||||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
self.remote_changedir(path.as_path(), push);
|
||||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
} else {
|
||||||
_ => {}
|
self.local_changedir(path.as_path(), push);
|
||||||
},
|
}
|
||||||
SyncBrowsingDestination::Path(_) => match self.browser.tab() {
|
}
|
||||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
|
||||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
/// Create a directory on the opposite side for sync browsing.
|
||||||
_ => {}
|
fn sync_mkdir_on_opposite(&mut self, name: String) {
|
||||||
},
|
if self.is_local_tab() {
|
||||||
SyncBrowsingDestination::PreviousDir => match self.browser.tab() {
|
self.action_remote_mkdir(name);
|
||||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), false),
|
} else {
|
||||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), false),
|
self.action_local_mkdir(name);
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,35 +158,34 @@ impl FileTransferActivity {
|
|||||||
&mut self,
|
&mut self,
|
||||||
destination: &SyncBrowsingDestination,
|
destination: &SyncBrowsingDestination,
|
||||||
) -> Option<PathBuf> {
|
) -> Option<PathBuf> {
|
||||||
match (destination, self.browser.tab()) {
|
let is_local = self.is_local_tab();
|
||||||
// NOTE: tab and methods are switched on purpose
|
match destination {
|
||||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::HostBridge) => {
|
// NOTE: tab and methods are switched on purpose (we resolve from the opposite side)
|
||||||
self.remote().wrkdir.parent().map(|x| x.to_path_buf())
|
SyncBrowsingDestination::ParentDir => {
|
||||||
}
|
if is_local {
|
||||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => {
|
self.remote().wrkdir.parent().map(|x| x.to_path_buf())
|
||||||
self.host_bridge().wrkdir.parent().map(|x| x.to_path_buf())
|
|
||||||
}
|
|
||||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::HostBridge) => {
|
|
||||||
if let Some(p) = self.remote_mut().popd() {
|
|
||||||
Some(p)
|
|
||||||
} else {
|
} else {
|
||||||
warn!("Cannot synchronize browsing: remote has no previous directory in stack");
|
self.host_bridge().wrkdir.parent().map(|x| x.to_path_buf())
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => {
|
SyncBrowsingDestination::PreviousDir => {
|
||||||
if let Some(p) = self.host_bridge_mut().popd() {
|
if is_local {
|
||||||
|
if let Some(p) = self.remote_mut().popd() {
|
||||||
|
Some(p)
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Cannot synchronize browsing: remote has no previous directory in stack"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else if let Some(p) = self.host_bridge_mut().popd() {
|
||||||
Some(p)
|
Some(p)
|
||||||
} else {
|
} else {
|
||||||
warn!("Cannot synchronize browsing: local has no previous directory in stack");
|
warn!("Cannot synchronize browsing: local has no previous directory in stack");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(SyncBrowsingDestination::Path(p), _) => Some(PathBuf::from(p.as_str())),
|
SyncBrowsingDestination::Path(p) => Some(PathBuf::from(p.as_str())),
|
||||||
_ => {
|
|
||||||
warn!("Cannot synchronize browsing for current explorer");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,83 +3,12 @@ use remotefs::fs::UnixPex;
|
|||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub fn action_local_chmod(&mut self, mode: UnixPex) {
|
/// Change file mode for the currently selected entries via the active tab's pane.
|
||||||
let files = self.get_local_selected_entries().get_files();
|
pub(crate) fn action_chmod(&mut self, mode: UnixPex) {
|
||||||
|
let files = self.get_selected_entries().get_files();
|
||||||
|
|
||||||
for file in files {
|
for file in files {
|
||||||
if let Err(err) = self.host_bridge.chmod(file.path(), mode) {
|
if let Err(err) = self.browser.fs_pane_mut().fs.chmod(file.path(), mode) {
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"could not change mode for {}: {}",
|
|
||||||
file.path().display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action_remote_chmod(&mut self, mode: UnixPex) {
|
|
||||||
let files = self.get_remote_selected_entries().get_files();
|
|
||||||
|
|
||||||
for file in files {
|
|
||||||
let mut metadata = file.metadata.clone();
|
|
||||||
metadata.mode = Some(mode);
|
|
||||||
|
|
||||||
if let Err(err) = self.client.setstat(file.path(), metadata) {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"could not change mode for {}: {}",
|
|
||||||
file.path().display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action_find_local_chmod(&mut self, mode: UnixPex) {
|
|
||||||
let files = self.get_found_selected_entries().get_files();
|
|
||||||
|
|
||||||
for file in files {
|
|
||||||
if let Err(err) = self.host_bridge.chmod(file.path(), mode) {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"could not change mode for {}: {}",
|
|
||||||
file.path().display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action_find_remote_chmod(&mut self, mode: UnixPex) {
|
|
||||||
let files = self.get_found_selected_entries().get_files();
|
|
||||||
|
|
||||||
for file in files {
|
|
||||||
let mut metadata = file.metadata.clone();
|
|
||||||
metadata.mode = Some(mode);
|
|
||||||
|
|
||||||
if let Err(err) = self.client.setstat(file.path(), metadata) {
|
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -1,61 +1,38 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use remotefs::{File, RemoteErrorType};
|
use remotefs::File;
|
||||||
|
|
||||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// Copy file on local
|
/// Copy the currently selected file(s) via the active tab's pane.
|
||||||
pub(crate) fn action_local_copy(&mut self, input: String) {
|
pub(crate) fn action_copy(&mut self, input: String) {
|
||||||
match self.get_local_selected_entries() {
|
match self.get_selected_entries() {
|
||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
let dest_path: PathBuf = PathBuf::from(input);
|
let dest_path = PathBuf::from(input);
|
||||||
self.local_copy_file(&entry, dest_path.as_path());
|
self.copy_file(entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
|
||||||
for (entry, mut dest_path) in entries.into_iter() {
|
for (entry, mut dest_path) in entries.into_iter() {
|
||||||
dest_path.push(entry.name());
|
dest_path.push(entry.name());
|
||||||
self.local_copy_file(&entry, dest_path.as_path());
|
self.copy_file(entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear selection
|
// clear selection and reload
|
||||||
self.host_bridge_mut().clear_queue();
|
self.browser.explorer_mut().clear_queue();
|
||||||
self.reload_host_bridge_filelist();
|
self.reload_browser_file_list();
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy file on remote
|
fn copy_file(&mut self, entry: File, dest: &Path) {
|
||||||
pub(crate) fn action_remote_copy(&mut self, input: String) {
|
match self.browser.fs_pane_mut().fs.copy(&entry, dest) {
|
||||||
match self.get_remote_selected_entries() {
|
|
||||||
SelectedFile::One(entry) => {
|
|
||||||
let dest_path: PathBuf = PathBuf::from(input);
|
|
||||||
self.remote_copy_file(entry, dest_path.as_path());
|
|
||||||
}
|
|
||||||
SelectedFile::Many(entries) => {
|
|
||||||
// Iter files
|
|
||||||
for (entry, mut dest_path) in entries.into_iter() {
|
|
||||||
dest_path.push(entry.name());
|
|
||||||
self.remote_copy_file(entry, dest_path.as_path());
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear selection
|
|
||||||
self.remote_mut().clear_queue();
|
|
||||||
self.reload_remote_filelist();
|
|
||||||
}
|
|
||||||
SelectedFile::None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn local_copy_file(&mut self, entry: &File, dest: &Path) {
|
|
||||||
match self.host_bridge.copy(entry, dest) {
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
@@ -66,45 +43,23 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => self.log_and_alert(
|
Err(err) => {
|
||||||
LogLevel::Error,
|
// On remote tabs, fall back to tricky_copy (download + re-upload)
|
||||||
format!(
|
// when the protocol doesn't support server-side copy.
|
||||||
"Could not copy \"{}\" to \"{}\": {}",
|
if !self.is_local_tab() {
|
||||||
entry.path().display(),
|
|
||||||
dest.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remote_copy_file(&mut self, entry: File, dest: &Path) {
|
|
||||||
match self.client.as_mut().copy(entry.path(), dest) {
|
|
||||||
Ok(_) => {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!(
|
|
||||||
"Copied \"{}\" to \"{}\"",
|
|
||||||
entry.path().display(),
|
|
||||||
dest.display()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => match err.kind {
|
|
||||||
RemoteErrorType::UnsupportedFeature => {
|
|
||||||
// If copy is not supported, perform the tricky copy
|
|
||||||
let _ = self.tricky_copy(entry, dest);
|
let _ = self.tricky_copy(entry, dest);
|
||||||
|
} else {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not copy \"{}\" to \"{}\": {}",
|
||||||
|
entry.path().display(),
|
||||||
|
dest.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_ => self.log_and_alert(
|
}
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not copy \"{}\" to \"{}\": {}",
|
|
||||||
entry.path().display(),
|
|
||||||
dest.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +93,12 @@ impl FileTransferActivity {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
// Stat dir
|
// Stat dir
|
||||||
let tempdir_entry = match self.host_bridge.stat(tempdir_path.as_path()) {
|
let tempdir_entry = match self
|
||||||
|
.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.stat(tempdir_path.as_path())
|
||||||
|
{
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
@@ -191,9 +151,17 @@ impl FileTransferActivity {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry = match self.host_bridge.stat(tmpfile.path()) {
|
let tmpfile_entry = match self.browser.local_pane_mut().fs.stat(tmpfile.path()) {
|
||||||
Ok(e) if e.is_file() => e,
|
Ok(e) if e.is_file() => e,
|
||||||
Ok(_) => panic!("{} is not a file", tmpfile.path().display()),
|
Ok(_) => {
|
||||||
|
let msg = format!(
|
||||||
|
"Copy failed: \"{}\" is not a file",
|
||||||
|
tmpfile.path().display()
|
||||||
|
);
|
||||||
|
error!("{msg}");
|
||||||
|
self.log_and_alert(LogLevel::Error, msg.clone());
|
||||||
|
return Err(msg);
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
@@ -8,72 +8,28 @@ use remotefs::File;
|
|||||||
use super::{FileTransferActivity, LogLevel, SelectedFile};
|
use super::{FileTransferActivity, LogLevel, SelectedFile};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_delete(&mut self) {
|
/// Delete the currently selected file(s) via the active tab's pane.
|
||||||
match self.get_local_selected_entries() {
|
pub(crate) fn action_delete(&mut self) {
|
||||||
|
match self.get_selected_entries() {
|
||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
// Delete file
|
self.remove_file(&entry);
|
||||||
self.local_remove_file(&entry);
|
|
||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
|
||||||
for (entry, _) in entries.iter() {
|
for (entry, _) in entries.iter() {
|
||||||
// Delete file
|
self.remove_file(entry);
|
||||||
self.local_remove_file(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear selection
|
// clear selection and reload
|
||||||
self.host_bridge_mut().clear_queue();
|
self.browser.explorer_mut().clear_queue();
|
||||||
self.reload_host_bridge_filelist();
|
self.reload_browser_file_list();
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_delete(&mut self) {
|
/// Remove a single file or directory via the active tab's pane.
|
||||||
match self.get_remote_selected_entries() {
|
pub(crate) fn remove_file(&mut self, entry: &File) {
|
||||||
SelectedFile::One(entry) => {
|
match self.browser.fs_pane_mut().fs.remove(entry) {
|
||||||
// Delete file
|
|
||||||
self.remote_remove_file(&entry);
|
|
||||||
}
|
|
||||||
SelectedFile::Many(entries) => {
|
|
||||||
// Iter files
|
|
||||||
for (entry, _) in entries.iter() {
|
|
||||||
// Delete file
|
|
||||||
self.remote_remove_file(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear selection
|
|
||||||
self.remote_mut().clear_queue();
|
|
||||||
self.reload_remote_filelist();
|
|
||||||
}
|
|
||||||
SelectedFile::None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn local_remove_file(&mut self, entry: &File) {
|
|
||||||
match self.host_bridge.remove(entry) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Log
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Removed file \"{}\"", entry.path().display()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not delete file \"{}\": {}",
|
|
||||||
entry.path().display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn remote_remove_file(&mut self, entry: &File) {
|
|
||||||
match self.client.remove_dir_all(entry.path()) {
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
@@ -10,6 +10,7 @@ use std::time::SystemTime;
|
|||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
use remotefs::fs::Metadata;
|
use remotefs::fs::Metadata;
|
||||||
|
|
||||||
|
use super::super::ui_result;
|
||||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -28,7 +29,7 @@ impl FileTransferActivity {
|
|||||||
format!("Opening file \"{}\"…", entry.path().display()),
|
format!("Opening file \"{}\"…", entry.path().display()),
|
||||||
);
|
);
|
||||||
// Edit file
|
// Edit file
|
||||||
let res = match self.host_bridge.is_localhost() {
|
let res = match self.browser.local_pane().fs.is_localhost() {
|
||||||
true => self.edit_local_file(entry.path()).map(|_| ()),
|
true => self.edit_local_file(entry.path()).map(|_| ()),
|
||||||
false => self.edit_bridged_local_file(entry),
|
false => self.edit_bridged_local_file(entry),
|
||||||
};
|
};
|
||||||
@@ -88,7 +89,7 @@ impl FileTransferActivity {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// open from host bridge
|
// open from host bridge
|
||||||
let mut reader = match self.host_bridge.open_file(entry.path()) {
|
let mut reader = match self.browser.local_pane_mut().fs.open_file(entry.path()) {
|
||||||
Ok(reader) => reader,
|
Ok(reader) => reader,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!("Failed to open bridged entry: {err}"));
|
return Err(format!("Failed to open bridged entry: {err}"));
|
||||||
@@ -122,7 +123,7 @@ impl FileTransferActivity {
|
|||||||
return Err(format!("Could not open file: {err}"));
|
return Err(format!("Could not open file: {err}"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut writer = match self.host_bridge.create_file(
|
let mut writer = match self.browser.local_pane_mut().fs.create_file(
|
||||||
entry.path(),
|
entry.path(),
|
||||||
&Metadata {
|
&Metadata {
|
||||||
size: new_file_size,
|
size: new_file_size,
|
||||||
@@ -139,7 +140,9 @@ impl FileTransferActivity {
|
|||||||
return Err(format!("Could not write file: {err}"));
|
return Err(format!("Could not write file: {err}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.host_bridge
|
self.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
.finalize_write(writer)
|
.finalize_write(writer)
|
||||||
.map_err(|err| format!("Could not write file: {err}"))?;
|
.map_err(|err| format!("Could not write file: {err}"))?;
|
||||||
}
|
}
|
||||||
@@ -177,7 +180,7 @@ impl FileTransferActivity {
|
|||||||
error!("Could not leave alternate screen: {}", err);
|
error!("Could not leave alternate screen: {}", err);
|
||||||
}
|
}
|
||||||
// Lock ports
|
// Lock ports
|
||||||
assert!(self.app.lock_ports().is_ok());
|
ui_result(self.app.lock_ports());
|
||||||
// Get current file modification time
|
// Get current file modification time
|
||||||
let prev_mtime = self.get_localhost_mtime(path)?;
|
let prev_mtime = self.get_localhost_mtime(path)?;
|
||||||
// Open editor
|
// Open editor
|
||||||
@@ -205,7 +208,7 @@ impl FileTransferActivity {
|
|||||||
error!("Could not clear screen screen: {}", err);
|
error!("Could not clear screen screen: {}", err);
|
||||||
}
|
}
|
||||||
// Unlock ports
|
// Unlock ports
|
||||||
assert!(self.app.unlock_ports().is_ok());
|
ui_result(self.app.unlock_ports());
|
||||||
}
|
}
|
||||||
let after_mtime = self.get_localhost_mtime(path)?;
|
let after_mtime = self.get_localhost_mtime(path)?;
|
||||||
|
|
||||||
@@ -241,7 +244,8 @@ impl FileTransferActivity {
|
|||||||
return Err(format!("Could not open file {file_name}: {err}"));
|
return Err(format!("Could not open file {file_name}: {err}"));
|
||||||
}
|
}
|
||||||
// Get current file modification time
|
// Get current file modification time
|
||||||
let prev_mtime: SystemTime = match self.host_bridge.stat(tmpfile.as_path()) {
|
let prev_mtime: SystemTime = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path())
|
||||||
|
{
|
||||||
Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH),
|
Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@@ -254,7 +258,7 @@ impl FileTransferActivity {
|
|||||||
// Edit file
|
// Edit file
|
||||||
self.edit_local_file(tmpfile.as_path())?;
|
self.edit_local_file(tmpfile.as_path())?;
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry: File = match self.host_bridge.stat(tmpfile.as_path()) {
|
let tmpfile_entry: File = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@@ -280,7 +284,7 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry = match self.host_bridge.stat(tmpfile.as_path()) {
|
let tmpfile_entry = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -37,15 +37,11 @@ impl FromStr for Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_exec(&mut self, input: String) {
|
pub(crate) fn action_exec_cmd(&mut self, input: String) {
|
||||||
self.action_exec(false, input);
|
self.action_exec(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
fn action_exec(&mut self, cmd: String) {
|
||||||
self.action_exec(true, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn action_exec(&mut self, remote: bool, cmd: String) {
|
|
||||||
if cmd.is_empty() {
|
if cmd.is_empty() {
|
||||||
self.print_terminal("".to_string());
|
self.print_terminal("".to_string());
|
||||||
}
|
}
|
||||||
@@ -61,10 +57,10 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
match cmd {
|
match cmd {
|
||||||
Command::Cd(path) => {
|
Command::Cd(path) => {
|
||||||
self.action_exec_cd(remote, path);
|
self.action_exec_cd(path);
|
||||||
}
|
}
|
||||||
Command::Exec(executable) => {
|
Command::Exec(executable) => {
|
||||||
self.action_exec_executable(remote, executable);
|
self.action_exec_executable(executable);
|
||||||
}
|
}
|
||||||
Command::Exit => {
|
Command::Exit => {
|
||||||
self.action_exec_exit();
|
self.action_exec_exit();
|
||||||
@@ -77,43 +73,20 @@ impl FileTransferActivity {
|
|||||||
self.umount_exec();
|
self.umount_exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action_exec_cd(&mut self, remote: bool, input: String) {
|
fn action_exec_cd(&mut self, input: String) {
|
||||||
let new_dir = if remote {
|
let dir_path = self.pane_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||||
let dir_path: PathBuf =
|
self.pane_changedir(dir_path.as_path(), true);
|
||||||
self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
|
||||||
self.remote_changedir(dir_path.as_path(), true);
|
|
||||||
|
|
||||||
dir_path
|
|
||||||
} else {
|
|
||||||
let dir_path: PathBuf =
|
|
||||||
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
|
||||||
self.host_bridge_changedir(dir_path.as_path(), true);
|
|
||||||
|
|
||||||
dir_path
|
|
||||||
};
|
|
||||||
|
|
||||||
self.update_browser_file_list();
|
self.update_browser_file_list();
|
||||||
|
|
||||||
// update prompt and print the new directory
|
// update prompt and print the new directory
|
||||||
self.update_terminal_prompt();
|
self.update_terminal_prompt();
|
||||||
self.print_terminal(new_dir.display().to_string());
|
self.print_terminal(dir_path.display().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a [`Command::Exec`] command
|
/// Execute a command via the active tab's pane.
|
||||||
fn action_exec_executable(&mut self, remote: bool, cmd: String) {
|
fn action_exec_executable(&mut self, cmd: String) {
|
||||||
let res = if remote {
|
match self.browser.fs_pane_mut().fs.exec(cmd.as_str()) {
|
||||||
self.client
|
|
||||||
.as_mut()
|
|
||||||
.exec(cmd.as_str())
|
|
||||||
.map(|(_, output)| output)
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
} else {
|
|
||||||
self.host_bridge
|
|
||||||
.exec(cmd.as_str())
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
self.print_terminal(output);
|
self.print_terminal(output);
|
||||||
}
|
}
|
||||||
@@ -122,7 +95,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Could not execute command \"{cmd}\": {err}"),
|
format!("Could not execute command \"{cmd}\": {err}"),
|
||||||
);
|
);
|
||||||
self.print_terminal(err);
|
self.print_terminal(err.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,14 @@
|
|||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
|
|
||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
enum Host {
|
|
||||||
HostBridge,
|
|
||||||
Remote,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
|
/// Calculate and display the total size of the selected file(s) via the active tab's pane.
|
||||||
pub(crate) fn action_get_file_size(&mut self) {
|
pub(crate) fn action_get_file_size(&mut self) {
|
||||||
// Get selected file
|
|
||||||
self.mount_blocking_wait("Getting total path size...");
|
self.mount_blocking_wait("Getting total path size...");
|
||||||
|
|
||||||
let total_size = match self.browser.tab() {
|
let files = self.get_selected_entries().get_files();
|
||||||
FileExplorerTab::HostBridge => {
|
let total_size = self.get_files_size(files);
|
||||||
let files = self.get_local_selected_entries().get_files();
|
|
||||||
self.get_files_size(files, Host::HostBridge)
|
|
||||||
}
|
|
||||||
FileExplorerTab::Remote => {
|
|
||||||
let files = self.get_remote_selected_entries().get_files();
|
|
||||||
self.get_files_size(files, Host::Remote)
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindHostBridge => {
|
|
||||||
let files = self.get_found_selected_entries().get_files();
|
|
||||||
self.get_files_size(files, Host::HostBridge)
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote => {
|
|
||||||
let files = self.get_found_selected_entries().get_files();
|
|
||||||
self.get_files_size(files, Host::Remote)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.umount_wait();
|
self.umount_wait();
|
||||||
self.mount_info(format!(
|
self.mount_info(format!(
|
||||||
@@ -40,24 +17,19 @@ impl FileTransferActivity {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_files_size(&mut self, files: Vec<File>, host: Host) -> u64 {
|
fn get_files_size(&mut self, files: Vec<File>) -> u64 {
|
||||||
files.into_iter().map(|f| self.get_file_size(f, host)).sum()
|
files.into_iter().map(|f| self.get_file_size(f)).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_file_size(&mut self, file: File, host: Host) -> u64 {
|
fn get_file_size(&mut self, file: File) -> u64 {
|
||||||
if let Some(symlink) = &file.metadata().symlink {
|
if let Some(symlink) = &file.metadata().symlink {
|
||||||
// stat
|
match self.browser.fs_pane_mut().fs.stat(symlink) {
|
||||||
let stat_res = match host {
|
|
||||||
Host::HostBridge => self.host_bridge.stat(symlink).map_err(|e| e.to_string()),
|
|
||||||
Host::Remote => self.client.stat(symlink).map_err(|e| e.to_string()),
|
|
||||||
};
|
|
||||||
match stat_res {
|
|
||||||
Ok(stat) => stat.metadata().size,
|
Ok(stat) => stat.metadata().size,
|
||||||
Err(err_msg) => {
|
Err(err) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Failed to stat symlink target {path}: {err_msg}",
|
"Failed to stat symlink target {path}: {err}",
|
||||||
path = symlink.display(),
|
path = symlink.display(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -65,22 +37,13 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if file.is_dir() {
|
} else if file.is_dir() {
|
||||||
// list and sum
|
match self.browser.fs_pane_mut().fs.list_dir(&file.path) {
|
||||||
let list_res = match host {
|
Ok(list) => list.into_iter().map(|f| self.get_file_size(f)).sum(),
|
||||||
Host::HostBridge => self
|
Err(err) => {
|
||||||
.host_bridge
|
|
||||||
.list_dir(&file.path)
|
|
||||||
.map_err(|e| e.to_string()),
|
|
||||||
Host::Remote => self.client.list_dir(&file.path).map_err(|e| e.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
match list_res {
|
|
||||||
Ok(list) => list.into_iter().map(|f| self.get_file_size(f, host)).sum(),
|
|
||||||
Err(err_msg) => {
|
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Failed to list directory {path}: {err_msg}",
|
"Failed to list directory {path}: {err}",
|
||||||
path = file.path.display(),
|
path = file.path.display(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::super::browser::FileExplorerTab;
|
|
||||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -22,33 +21,19 @@ impl FileTransferActivity {
|
|||||||
Some(p) => p.to_path_buf(),
|
Some(p) => p.to_path_buf(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Change directory
|
// Change directory on the active tab's pane
|
||||||
match self.browser.tab() {
|
self.pane_changedir(path.as_path(), true);
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
|
||||||
self.host_bridge_changedir(path.as_path(), true)
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
self.remote_changedir(path.as_path(), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
|
pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
|
||||||
let wrkdir: PathBuf = match self.browser.tab() {
|
let wrkdir: PathBuf = self.browser.opposite_pane().explorer.wrkdir.clone();
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
|
||||||
self.remote().wrkdir.clone()
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
self.host_bridge().wrkdir.clone()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match self.get_found_selected_entries() {
|
match self.get_found_selected_entries() {
|
||||||
SelectedFile::One(entry) => match self.browser.tab() {
|
SelectedFile::One(entry) => {
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
if self.is_local_tab() {
|
||||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||||
if self.config().get_prompt_on_file_replace()
|
if self.config().get_prompt_on_file_replace()
|
||||||
&& self.remote_file_exists(file_to_check.as_path())
|
&& self.file_exists(file_to_check.as_path(), false)
|
||||||
&& !self.should_replace_file(
|
&& !self.should_replace_file(
|
||||||
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
||||||
)
|
)
|
||||||
@@ -66,11 +51,10 @@ impl FileTransferActivity {
|
|||||||
format!("Could not upload file: {err}"),
|
format!("Could not upload file: {err}"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||||
if self.config().get_prompt_on_file_replace()
|
if self.config().get_prompt_on_file_replace()
|
||||||
&& self.host_bridge_file_exists(file_to_check.as_path())
|
&& self.file_exists(file_to_check.as_path(), true)
|
||||||
&& !self.should_replace_file(
|
&& !self.should_replace_file(
|
||||||
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
||||||
)
|
)
|
||||||
@@ -89,7 +73,7 @@ impl FileTransferActivity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// In case of selection: save multiple files in wrkdir/input
|
// In case of selection: save multiple files in wrkdir/input
|
||||||
let mut dest_path: PathBuf = wrkdir;
|
let mut dest_path: PathBuf = wrkdir;
|
||||||
@@ -97,58 +81,51 @@ impl FileTransferActivity {
|
|||||||
dest_path.push(save_as);
|
dest_path.push(save_as);
|
||||||
}
|
}
|
||||||
// Iter files
|
// Iter files
|
||||||
match self.browser.tab() {
|
if self.is_local_tab() {
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
|
||||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
self.get_files_to_transfer_with_overwrites(
|
||||||
entries,
|
|
||||||
) = self.get_files_to_transfer_with_overwrites(
|
|
||||||
entries,
|
entries,
|
||||||
super::save::CheckFileExists::Remote,
|
super::save::CheckFileExists::Remote,
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
debug!("User cancelled file transfer due to overwrites");
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Err(err) = self.filetransfer_send(
|
if let Err(err) = self.filetransfer_send(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
{
|
self.log_and_alert(
|
||||||
self.log_and_alert(
|
LogLevel::Error,
|
||||||
LogLevel::Error,
|
format!("Could not upload file: {err}"),
|
||||||
format!("Could not upload file: {err}"),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
} else {
|
||||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
|
||||||
entries,
|
self.get_files_to_transfer_with_overwrites(
|
||||||
) = self.get_files_to_transfer_with_overwrites(
|
|
||||||
entries,
|
entries,
|
||||||
super::save::CheckFileExists::HostBridge,
|
super::save::CheckFileExists::HostBridge,
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
debug!("User cancelled file transfer due to overwrites");
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Err(err) = self.filetransfer_recv(
|
if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Could not download file: {err}"),
|
format!("Could not download file: {err}"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear selection
|
// clear selection
|
||||||
if let Some(f) = self.found_mut() {
|
if let Some(f) = self.found_mut() {
|
||||||
f.clear_queue();
|
f.clear_queue();
|
||||||
self.update_find_list();
|
self.update_find_list();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,14 +136,11 @@ impl FileTransferActivity {
|
|||||||
pub(crate) fn action_find_delete(&mut self) {
|
pub(crate) fn action_find_delete(&mut self) {
|
||||||
match self.get_found_selected_entries() {
|
match self.get_found_selected_entries() {
|
||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
// Delete file
|
self.remove_file(&entry);
|
||||||
self.remove_found_file(&entry);
|
|
||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
|
||||||
for (entry, _) in entries.iter() {
|
for (entry, _) in entries.iter() {
|
||||||
// Delete file
|
self.remove_file(entry);
|
||||||
self.remove_found_file(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear selection
|
// clear selection
|
||||||
@@ -179,17 +153,6 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_found_file(&mut self, entry: &File) {
|
|
||||||
match self.browser.tab() {
|
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
|
||||||
self.local_remove_file(entry);
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
self.remote_remove_file(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn action_find_open(&mut self) {
|
pub(crate) fn action_find_open(&mut self) {
|
||||||
match self.get_found_selected_entries() {
|
match self.get_found_selected_entries() {
|
||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
@@ -235,13 +198,6 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn open_found_file(&mut self, entry: &File, with: Option<&str>) {
|
fn open_found_file(&mut self, entry: &File, with: Option<&str>) {
|
||||||
match self.browser.tab() {
|
self.open_file(entry, with);
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
|
||||||
self.action_open_local_file(entry, with);
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
|
||||||
self.action_open_remote_file(entry, with);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use super::FileTransferActivity;
|
use super::FileTransferActivity;
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,46 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use remotefs::fs::UnixPex;
|
|
||||||
|
|
||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_mkdir(&mut self, input: String) {
|
/// Create a directory via the active tab's pane.
|
||||||
match self
|
pub(crate) fn action_mkdir(&mut self, input: String) {
|
||||||
.host_bridge
|
let path = PathBuf::from(input.as_str());
|
||||||
.mkdir(PathBuf::from(input.as_str()).as_path())
|
match self.browser.fs_pane_mut().fs.mkdir(path.as_path()) {
|
||||||
{
|
Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")),
|
||||||
Ok(_) => {
|
Err(err) => self.log_and_alert(
|
||||||
// Reload files
|
LogLevel::Error,
|
||||||
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
format!("Could not create directory \"{input}\": {err}"),
|
||||||
}
|
),
|
||||||
Err(err) => {
|
|
||||||
// Report err
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not create directory \"{input}\": {err}"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub(crate) fn action_remote_mkdir(&mut self, input: String) {
|
|
||||||
match self.client.as_mut().create_dir(
|
/// Create a directory on the local host (used by sync-browsing in change_dir).
|
||||||
PathBuf::from(input.as_str()).as_path(),
|
pub(in crate::ui::activities::filetransfer) fn action_local_mkdir(&mut self, input: String) {
|
||||||
UnixPex::from(0o755),
|
let path = PathBuf::from(input.as_str());
|
||||||
) {
|
match self.browser.local_pane_mut().fs.mkdir(path.as_path()) {
|
||||||
Ok(_) => {
|
Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")),
|
||||||
// Reload files
|
Err(err) => self.log_and_alert(
|
||||||
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
LogLevel::Error,
|
||||||
}
|
format!("Could not create directory \"{input}\": {err}"),
|
||||||
Err(err) => {
|
),
|
||||||
// Report err
|
}
|
||||||
self.log_and_alert(
|
}
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not create directory \"{input}\": {err}"),
|
/// Create a directory on the remote host (used by sync-browsing in change_dir).
|
||||||
);
|
pub(in crate::ui::activities::filetransfer) fn action_remote_mkdir(&mut self, input: String) {
|
||||||
}
|
let path = PathBuf::from(input.as_str());
|
||||||
|
match self.browser.remote_pane_mut().fs.mkdir(path.as_path()) {
|
||||||
|
Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")),
|
||||||
|
Err(err) => self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not create directory \"{input}\": {err}"),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -92,36 +92,43 @@ impl FileTransferActivity {
|
|||||||
self.get_selected_files(&Id::ExplorerHostBridge)
|
self.get_selected_files(&Id::ExplorerHostBridge)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_local_selected_file(&self) -> Option<File> {
|
|
||||||
self.get_selected_file(&Id::ExplorerHostBridge)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get remote file entry
|
/// Get remote file entry
|
||||||
pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile {
|
pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile {
|
||||||
self.get_selected_files(&Id::ExplorerRemote)
|
self.get_selected_files(&Id::ExplorerRemote)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_remote_selected_file(&self) -> Option<File> {
|
|
||||||
self.get_selected_file(&Id::ExplorerRemote)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether only one entry is selected on local host
|
|
||||||
pub(crate) fn is_local_selected_one(&mut self) -> bool {
|
|
||||||
matches!(self.get_local_selected_entries(), SelectedFile::One(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether only one entry is selected on remote host
|
|
||||||
pub(crate) fn is_remote_selected_one(&mut self) -> bool {
|
|
||||||
matches!(self.get_remote_selected_entries(), SelectedFile::One(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get remote file entry
|
/// Get remote file entry
|
||||||
pub(crate) fn get_found_selected_entries(&mut self) -> SelectedFile {
|
pub(crate) fn get_found_selected_entries(&mut self) -> SelectedFile {
|
||||||
self.get_selected_files(&Id::ExplorerFind)
|
self.get_selected_files(&Id::ExplorerFind)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_found_selected_file(&self) -> Option<File> {
|
pub(crate) fn get_found_selected_file(&self) -> Option<File> {
|
||||||
self.get_selected_file(&Id::ExplorerFind)
|
self.get_selected_file_by_id(&Id::ExplorerFind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get selected entries from whichever tab is currently active.
|
||||||
|
pub(crate) fn get_selected_entries(&mut self) -> SelectedFile {
|
||||||
|
let id = match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge => Id::ExplorerHostBridge,
|
||||||
|
FileExplorerTab::Remote => Id::ExplorerRemote,
|
||||||
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => Id::ExplorerFind,
|
||||||
|
};
|
||||||
|
self.get_selected_files(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get single selected file from whichever tab is currently active.
|
||||||
|
pub(crate) fn get_selected_file(&self) -> Option<File> {
|
||||||
|
let id = match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge => Id::ExplorerHostBridge,
|
||||||
|
FileExplorerTab::Remote => Id::ExplorerRemote,
|
||||||
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => Id::ExplorerFind,
|
||||||
|
};
|
||||||
|
self.get_selected_file_by_id(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether only one entry is selected on the current tab.
|
||||||
|
pub(crate) fn is_selected_one(&mut self) -> bool {
|
||||||
|
matches!(self.get_selected_entries(), SelectedFile::One(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- private
|
// -- private
|
||||||
@@ -164,13 +171,13 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
fn get_file_from_path(&mut self, id: &Id, path: &Path) -> Option<File> {
|
fn get_file_from_path(&mut self, id: &Id, path: &Path) -> Option<File> {
|
||||||
match *id {
|
match *id {
|
||||||
Id::ExplorerHostBridge => self.host_bridge.stat(path).ok(),
|
Id::ExplorerHostBridge => self.browser.local_pane_mut().fs.stat(path).ok(),
|
||||||
Id::ExplorerRemote => self.client.stat(path).ok(),
|
Id::ExplorerRemote => self.browser.remote_pane_mut().fs.stat(path).ok(),
|
||||||
Id::ExplorerFind => {
|
Id::ExplorerFind => {
|
||||||
let found = self.browser.found_tab().unwrap();
|
let found = self.browser.found_tab().unwrap();
|
||||||
match found {
|
match found {
|
||||||
FoundExplorerTab::Local => self.host_bridge.stat(path).ok(),
|
FoundExplorerTab::Local => self.browser.local_pane_mut().fs.stat(path).ok(),
|
||||||
FoundExplorerTab::Remote => self.client.stat(path).ok(),
|
FoundExplorerTab::Remote => self.browser.remote_pane_mut().fs.stat(path).ok(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -182,11 +189,16 @@ impl FileTransferActivity {
|
|||||||
Id::ExplorerHostBridge => self.host_bridge(),
|
Id::ExplorerHostBridge => self.host_bridge(),
|
||||||
Id::ExplorerRemote => self.remote(),
|
Id::ExplorerRemote => self.remote(),
|
||||||
Id::ExplorerFind => self.found().as_ref().unwrap(),
|
Id::ExplorerFind => self.found().as_ref().unwrap(),
|
||||||
_ => unreachable!(),
|
_ => {
|
||||||
|
error!(
|
||||||
|
"browser_by_id called with unexpected id: {id:?}; falling back to local explorer"
|
||||||
|
);
|
||||||
|
self.host_bridge()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_selected_file(&self, id: &Id) -> Option<File> {
|
fn get_selected_file_by_id(&self, id: &Id) -> Option<File> {
|
||||||
let browser = self.browser_by_id(id);
|
let browser = self.browser_by_id(id);
|
||||||
// if no transfer queue, return selected files
|
// if no transfer queue, return selected files
|
||||||
match self.get_selected_index(id) {
|
match self.get_selected_index(id) {
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::fs::File as StdFile;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use remotefs::fs::Metadata;
|
use remotefs::fs::Metadata;
|
||||||
|
|
||||||
use super::{File, FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_newfile(&mut self, input: String) {
|
/// Create a new empty file via the active tab's pane.
|
||||||
// Check if file exists
|
pub(crate) fn action_newfile(&mut self, input: String) {
|
||||||
let mut file_exists: bool = false;
|
// Check if file exists in current explorer listing
|
||||||
for file in self.host_bridge().iter_files_all() {
|
if self
|
||||||
if input == file.name() {
|
.browser
|
||||||
file_exists = true;
|
.explorer()
|
||||||
}
|
.iter_files_all()
|
||||||
}
|
.any(|file| input == file.name())
|
||||||
if file_exists {
|
{
|
||||||
self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",));
|
self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create file
|
let file_path = PathBuf::from(input.as_str());
|
||||||
let file_path: PathBuf = PathBuf::from(input.as_str());
|
|
||||||
let writer = match self
|
let writer = match self
|
||||||
.host_bridge
|
.browser
|
||||||
|
.fs_pane_mut()
|
||||||
|
.fs
|
||||||
.create_file(file_path.as_path(), &Metadata::default())
|
.create_file(file_path.as_path(), &Metadata::default())
|
||||||
{
|
{
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
@@ -40,7 +40,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// finalize write
|
// finalize write
|
||||||
if let Err(err) = self.host_bridge.finalize_write(writer) {
|
if let Err(err) = self.browser.fs_pane_mut().fs.finalize_write(writer) {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Could not write file \"{}\": {}", file_path.display(), err),
|
format!("Could not write file \"{}\": {}", file_path.display(), err),
|
||||||
@@ -53,67 +53,4 @@ impl FileTransferActivity {
|
|||||||
format!("Created file \"{}\"", file_path.display()),
|
format!("Created file \"{}\"", file_path.display()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_newfile(&mut self, input: String) {
|
|
||||||
// Check if file exists
|
|
||||||
let mut file_exists: bool = false;
|
|
||||||
for file in self.remote().iter_files_all() {
|
|
||||||
if input == file.name() {
|
|
||||||
file_exists = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if file_exists {
|
|
||||||
self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Get path on remote
|
|
||||||
let file_path: PathBuf = PathBuf::from(input.as_str());
|
|
||||||
// Create file (on local)
|
|
||||||
match tempfile::NamedTempFile::new() {
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(LogLevel::Error, format!("Could not create tempfile: {err}"))
|
|
||||||
}
|
|
||||||
Ok(tfile) => {
|
|
||||||
// Stat tempfile
|
|
||||||
let local_file: File = match self.host_bridge.stat(tfile.path()) {
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not stat tempfile: {err}"),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Ok(f) => f,
|
|
||||||
};
|
|
||||||
if local_file.is_file() {
|
|
||||||
// Create file
|
|
||||||
let reader = Box::new(match StdFile::open(tfile.path()) {
|
|
||||||
Ok(f) => f,
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not open tempfile: {err}"),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
match self
|
|
||||||
.client
|
|
||||||
.create_file(file_path.as_path(), &local_file.metadata, reader)
|
|
||||||
{
|
|
||||||
Err(err) => self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not create file \"{}\": {}", file_path.display(), err),
|
|
||||||
),
|
|
||||||
Ok(_) => {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Created file \"{}\"", file_path.display()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,50 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
// ext
|
// ext
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
use super::{File, FileTransferActivity, LogLevel, TransferPayload};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// Open local file
|
/// Open selected file(s) with default application
|
||||||
pub(crate) fn action_open_local(&mut self) {
|
pub(crate) fn action_open(&mut self) {
|
||||||
let entries: Vec<File> = match self.get_local_selected_entries() {
|
let entries = self.get_selected_entries().get_files();
|
||||||
SelectedFile::One(entry) => vec![entry],
|
entries.iter().for_each(|x| self.open_file(x, None));
|
||||||
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
|
||||||
SelectedFile::None => vec![],
|
|
||||||
};
|
|
||||||
entries
|
|
||||||
.iter()
|
|
||||||
.for_each(|x| self.action_open_local_file(x, None));
|
|
||||||
|
|
||||||
// clear selection
|
// clear queue
|
||||||
self.host_bridge_mut().clear_queue();
|
self.browser.explorer_mut().clear_queue();
|
||||||
self.reload_host_bridge_filelist();
|
self.reload_browser_file_list();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open local file
|
/// Open selected file(s) with provided application
|
||||||
pub(crate) fn action_open_remote(&mut self) {
|
pub(crate) fn action_open_with(&mut self, with: &str) {
|
||||||
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
let entries = self.get_selected_entries().get_files();
|
||||||
SelectedFile::One(entry) => vec![entry],
|
entries.iter().for_each(|x| self.open_file(x, Some(with)));
|
||||||
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
|
||||||
SelectedFile::None => vec![],
|
|
||||||
};
|
|
||||||
entries
|
|
||||||
.iter()
|
|
||||||
.for_each(|x| self.action_open_remote_file(x, None));
|
|
||||||
|
|
||||||
// clear selection
|
// clear queue
|
||||||
self.remote_mut().clear_queue();
|
self.browser.explorer_mut().clear_queue();
|
||||||
self.reload_remote_filelist();
|
self.reload_browser_file_list();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform open lopcal file
|
/// Open a file, dispatching based on whether the active pane is localhost.
|
||||||
pub(crate) fn action_open_local_file(&mut self, entry: &File, open_with: Option<&str>) {
|
pub(crate) fn open_file(&mut self, entry: &File, open_with: Option<&str>) {
|
||||||
if self.host_bridge.is_localhost() {
|
if self.browser.fs_pane().fs.is_localhost() {
|
||||||
|
// Direct open from local path
|
||||||
self.open_path_with(entry.path(), open_with);
|
self.open_path_with(entry.path(), open_with);
|
||||||
} else {
|
} else if self.is_local_tab() {
|
||||||
|
// Non-localhost host bridge: download via HostBridge API
|
||||||
self.open_bridged_file(entry, open_with);
|
self.open_bridged_file(entry, open_with);
|
||||||
|
} else {
|
||||||
|
// Remote: download via filetransfer_recv
|
||||||
|
self.action_open_remote_file(entry, open_with);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open remote file. The file is first downloaded to a temporary directory on localhost
|
/// Open remote file. The file is first downloaded to a temporary directory on localhost
|
||||||
pub(crate) fn action_open_remote_file(&mut self, entry: &File, open_with: Option<&str>) {
|
fn action_open_remote_file(&mut self, entry: &File, open_with: Option<&str>) {
|
||||||
// Download file
|
// Download file
|
||||||
let tmpfile: String =
|
let tmpfile: String =
|
||||||
match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) {
|
match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) {
|
||||||
@@ -90,39 +83,6 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open selected file with provided application
|
|
||||||
pub(crate) fn action_local_open_with(&mut self, with: &str) {
|
|
||||||
let entries: Vec<File> = match self.get_local_selected_entries() {
|
|
||||||
SelectedFile::One(entry) => vec![entry],
|
|
||||||
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
|
||||||
SelectedFile::None => vec![],
|
|
||||||
};
|
|
||||||
// Open all entries
|
|
||||||
entries
|
|
||||||
.iter()
|
|
||||||
.for_each(|x| self.action_open_local_file(x, Some(with)));
|
|
||||||
|
|
||||||
// clear selection
|
|
||||||
self.host_bridge_mut().clear_queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open selected file with provided application
|
|
||||||
pub(crate) fn action_remote_open_with(&mut self, with: &str) {
|
|
||||||
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
|
||||||
SelectedFile::One(entry) => vec![entry],
|
|
||||||
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
|
||||||
SelectedFile::None => vec![],
|
|
||||||
};
|
|
||||||
// Open all entries
|
|
||||||
entries
|
|
||||||
.iter()
|
|
||||||
.for_each(|x| self.action_open_remote_file(x, Some(with)));
|
|
||||||
|
|
||||||
// clear selection
|
|
||||||
self.remote_mut().clear_queue();
|
|
||||||
self.reload_remote_filelist();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_bridged_file(&mut self, entry: &File, open_with: Option<&str>) {
|
fn open_bridged_file(&mut self, entry: &File, open_with: Option<&str>) {
|
||||||
// Download file
|
// Download file
|
||||||
let tmpfile: String =
|
let tmpfile: String =
|
||||||
@@ -144,7 +104,7 @@ impl FileTransferActivity {
|
|||||||
let tmpfile = cache.join(tmpfile);
|
let tmpfile = cache.join(tmpfile);
|
||||||
|
|
||||||
// open from host bridge
|
// open from host bridge
|
||||||
let mut reader = match self.host_bridge.open_file(entry.path()) {
|
let mut reader = match self.browser.local_pane_mut().fs.open_file(entry.path()) {
|
||||||
Ok(reader) => reader,
|
Ok(reader) => reader,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log(
|
self.log(
|
||||||
|
|||||||
@@ -1,62 +1,38 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use remotefs::RemoteErrorType;
|
|
||||||
|
|
||||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile};
|
use super::{File, FileTransferActivity, LogLevel, SelectedFile};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_rename(&mut self, input: String) {
|
/// Rename / move the currently selected entries via the active tab's pane.
|
||||||
match self.get_local_selected_entries() {
|
pub(crate) fn action_rename(&mut self, input: String) {
|
||||||
|
match self.get_selected_entries() {
|
||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
let dest_path: PathBuf = PathBuf::from(input);
|
let dest_path = PathBuf::from(input);
|
||||||
self.local_rename_file(&entry, dest_path.as_path());
|
self.rename_file(&entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Try to copy each file to Input/{FILE_NAME}
|
|
||||||
// Iter files
|
|
||||||
for (entry, mut dest_path) in entries.into_iter() {
|
for (entry, mut dest_path) in entries.into_iter() {
|
||||||
dest_path.push(entry.name());
|
dest_path.push(entry.name());
|
||||||
self.local_rename_file(&entry, dest_path.as_path());
|
self.rename_file(&entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear selection
|
// clear selection and reload
|
||||||
self.host_bridge_mut().clear_queue();
|
self.browser.explorer_mut().clear_queue();
|
||||||
self.reload_host_bridge_filelist();
|
self.reload_browser_file_list();
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_rename(&mut self, input: String) {
|
/// Rename a single file via the active tab's pane.
|
||||||
match self.get_remote_selected_entries() {
|
/// Falls back to `tricky_move` on remote tabs when rename fails.
|
||||||
SelectedFile::One(entry) => {
|
fn rename_file(&mut self, entry: &File, dest: &Path) {
|
||||||
let dest_path: PathBuf = PathBuf::from(input);
|
match self.browser.fs_pane_mut().fs.rename(entry, dest) {
|
||||||
self.remote_rename_file(&entry, dest_path.as_path());
|
|
||||||
}
|
|
||||||
SelectedFile::Many(entries) => {
|
|
||||||
// Try to copy each file to Input/{FILE_NAME}
|
|
||||||
// Iter files
|
|
||||||
for (entry, mut dest_path) in entries.into_iter() {
|
|
||||||
dest_path.push(entry.name());
|
|
||||||
self.remote_rename_file(&entry, dest_path.as_path());
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear selection
|
|
||||||
self.remote_mut().clear_queue();
|
|
||||||
// reload remote
|
|
||||||
self.reload_remote_filelist();
|
|
||||||
}
|
|
||||||
SelectedFile::None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn local_rename_file(&mut self, entry: &File, dest: &Path) {
|
|
||||||
match self.host_bridge.rename(entry, dest) {
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
@@ -67,20 +43,31 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => self.log_and_alert(
|
Err(err) => {
|
||||||
LogLevel::Error,
|
if !self.is_local_tab() {
|
||||||
format!(
|
// Try tricky_move as a fallback on remote
|
||||||
"Could not move \"{}\" to \"{}\": {}",
|
debug!("Rename failed ({err}); attempting tricky_move");
|
||||||
entry.path().display(),
|
self.tricky_move(entry, dest);
|
||||||
dest.display(),
|
} else {
|
||||||
err
|
self.log_and_alert(
|
||||||
),
|
LogLevel::Error,
|
||||||
),
|
format!(
|
||||||
|
"Could not move \"{}\" to \"{}\": {}",
|
||||||
|
entry.path().display(),
|
||||||
|
dest.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rename / move a file on the remote host.
|
||||||
|
/// Falls back to `tricky_move` when the rename fails.
|
||||||
|
/// Also used by fswatcher for syncing renames.
|
||||||
pub(crate) fn remote_rename_file(&mut self, entry: &File, dest: &Path) {
|
pub(crate) fn remote_rename_file(&mut self, entry: &File, dest: &Path) {
|
||||||
match self.client.as_mut().mov(entry.path(), dest) {
|
match self.browser.remote_pane_mut().fs.rename(entry, dest) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
@@ -91,18 +78,11 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => {
|
Err(err) => {
|
||||||
|
// Try tricky_move as a fallback
|
||||||
|
debug!("Rename failed ({err}); attempting tricky_move");
|
||||||
self.tricky_move(entry, dest);
|
self.tricky_move(entry, dest);
|
||||||
}
|
}
|
||||||
Err(err) => self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not move \"{}\" to \"{}\": {}",
|
|
||||||
entry.path().display(),
|
|
||||||
dest.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +97,7 @@ impl FileTransferActivity {
|
|||||||
if self.tricky_copy(entry.clone(), dest).is_ok() {
|
if self.tricky_copy(entry.clone(), dest).is_ok() {
|
||||||
// Delete remote existing entry
|
// Delete remote existing entry
|
||||||
debug!("Tricky-copy worked; removing existing remote entry");
|
debug!("Tricky-copy worked; removing existing remote entry");
|
||||||
match self.client.remove_dir_all(entry.path()) {
|
match self.browser.remote_pane_mut().fs.remove(entry) {
|
||||||
Ok(_) => self.log(
|
Ok(_) => self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -42,20 +42,20 @@ enum AllOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_saveas(&mut self, input: String) {
|
pub(crate) fn action_saveas(&mut self, input: String) {
|
||||||
self.local_send_file(TransferOpts::default().save_as(Some(input)));
|
if self.is_local_tab() {
|
||||||
|
self.local_send_file(TransferOpts::default().save_as(Some(input)));
|
||||||
|
} else {
|
||||||
|
self.remote_recv_file(TransferOpts::default().save_as(Some(input)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_saveas(&mut self, input: String) {
|
pub(crate) fn action_transfer_file(&mut self) {
|
||||||
self.remote_recv_file(TransferOpts::default().save_as(Some(input)));
|
if self.is_local_tab() {
|
||||||
}
|
self.local_send_file(TransferOpts::default());
|
||||||
|
} else {
|
||||||
pub(crate) fn action_local_send(&mut self) {
|
self.remote_recv_file(TransferOpts::default());
|
||||||
self.local_send_file(TransferOpts::default());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn action_remote_recv(&mut self) {
|
|
||||||
self.remote_recv_file(TransferOpts::default());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_send_file(&mut self, opts: TransferOpts) {
|
fn local_send_file(&mut self, opts: TransferOpts) {
|
||||||
@@ -64,7 +64,7 @@ impl FileTransferActivity {
|
|||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||||
if self.config().get_prompt_on_file_replace()
|
if self.config().get_prompt_on_file_replace()
|
||||||
&& self.remote_file_exists(file_to_check.as_path())
|
&& self.file_exists(file_to_check.as_path(), false)
|
||||||
&& !self
|
&& !self
|
||||||
.should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name()))
|
.should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name()))
|
||||||
{
|
{
|
||||||
@@ -124,7 +124,7 @@ impl FileTransferActivity {
|
|||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||||
if self.config().get_prompt_on_file_replace()
|
if self.config().get_prompt_on_file_replace()
|
||||||
&& self.host_bridge_file_exists(file_to_check.as_path())
|
&& self.file_exists(file_to_check.as_path(), true)
|
||||||
&& !self
|
&& !self
|
||||||
.should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name()))
|
.should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name()))
|
||||||
{
|
{
|
||||||
@@ -307,8 +307,8 @@ impl FileTransferActivity {
|
|||||||
files.into_iter().partition(|(x, dest_path)| {
|
files.into_iter().partition(|(x, dest_path)| {
|
||||||
let p = Self::file_to_check_many(x, dest_path);
|
let p = Self::file_to_check_many(x, dest_path);
|
||||||
match file_exists {
|
match file_exists {
|
||||||
CheckFileExists::Remote => self.remote_file_exists(p.as_path()),
|
CheckFileExists::Remote => self.file_exists(p.as_path(), false),
|
||||||
CheckFileExists::HostBridge => self.host_bridge_file_exists(p.as_path()),
|
CheckFileExists::HostBridge => self.file_exists(p.as_path(), true),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::{File, FileTransferActivity};
|
use super::{File, FileTransferActivity};
|
||||||
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
|
/// List directory contents via the active tab's pane.
|
||||||
pub(crate) fn action_scan(&mut self, p: &Path) -> Result<Vec<File>, String> {
|
pub(crate) fn action_scan(&mut self, p: &Path) -> Result<Vec<File>, String> {
|
||||||
match self.browser.tab() {
|
self.browser
|
||||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => self
|
.fs_pane_mut()
|
||||||
.host_bridge
|
.fs
|
||||||
.list_dir(p)
|
.list_dir(p)
|
||||||
.map_err(|e| format!("Failed to list directory: {}", e)),
|
.map_err(|e| format!("Failed to list directory: {}", e))
|
||||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self
|
|
||||||
.client
|
|
||||||
.list_dir(p)
|
|
||||||
.map_err(|e| format!("Failed to list directory: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use super::{File, FileTransferActivity};
|
use super::{File, FileTransferActivity};
|
||||||
@@ -11,16 +11,15 @@ enum SubmitAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// Decides which action to perform on submit for local explorer
|
/// Decides which action to perform on submit, dispatching via the active tab's pane.
|
||||||
/// Return true whether the directory changed
|
pub(crate) fn action_submit(&mut self, entry: File) {
|
||||||
pub(crate) fn action_submit_local(&mut self, entry: File) {
|
|
||||||
let (action, entry) = if entry.is_dir() {
|
let (action, entry) = if entry.is_dir() {
|
||||||
(SubmitAction::ChangeDir, entry)
|
(SubmitAction::ChangeDir, entry)
|
||||||
} else if entry.metadata().symlink.is_some() {
|
} else if entry.metadata().symlink.is_some() {
|
||||||
// Stat file
|
// Stat symlink target via the active pane
|
||||||
let symlink = entry.metadata().symlink.as_ref().unwrap();
|
let symlink = entry.metadata().symlink.as_ref().unwrap();
|
||||||
let stat_file = match self.host_bridge.stat(symlink.as_path()) {
|
match self.browser.fs_pane_mut().fs.stat(symlink.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => (SubmitAction::ChangeDir, e),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
"Could not stat file pointed by {} ({}): {}",
|
"Could not stat file pointed by {} ({}): {}",
|
||||||
@@ -28,44 +27,14 @@ impl FileTransferActivity {
|
|||||||
symlink.display(),
|
symlink.display(),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
entry
|
(SubmitAction::ChangeDir, entry)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
(SubmitAction::ChangeDir, stat_file)
|
|
||||||
} else {
|
} else {
|
||||||
(SubmitAction::None, entry)
|
(SubmitAction::None, entry)
|
||||||
};
|
};
|
||||||
if let (SubmitAction::ChangeDir, entry) = (action, entry) {
|
if let (SubmitAction::ChangeDir, entry) = (action, entry) {
|
||||||
self.action_enter_local_dir(entry)
|
self.action_enter_dir(entry)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decides which action to perform on submit for remote explorer
|
|
||||||
/// Return true whether the directory changed
|
|
||||||
pub(crate) fn action_submit_remote(&mut self, entry: File) {
|
|
||||||
let (action, entry) = if entry.is_dir() {
|
|
||||||
(SubmitAction::ChangeDir, entry)
|
|
||||||
} else if entry.metadata().symlink.is_some() {
|
|
||||||
// Stat file
|
|
||||||
let symlink = entry.metadata().symlink.as_ref().unwrap();
|
|
||||||
let stat_file = match self.client.stat(symlink.as_path()) {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(err) => {
|
|
||||||
warn!(
|
|
||||||
"Could not stat file pointed by {} ({}): {}",
|
|
||||||
entry.path().display(),
|
|
||||||
symlink.display(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
entry
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(SubmitAction::ChangeDir, stat_file)
|
|
||||||
} else {
|
|
||||||
(SubmitAction::None, entry)
|
|
||||||
};
|
|
||||||
if let (SubmitAction::ChangeDir, entry) = (action, entry) {
|
|
||||||
self.action_enter_remote_dir(entry)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -8,57 +8,39 @@ use std::path::PathBuf;
|
|||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// Create symlink on localhost
|
/// Create a symlink for the currently selected file via the active tab's pane.
|
||||||
pub(crate) fn action_local_symlink(&mut self, name: String) {
|
pub(crate) fn action_symlink(&mut self, name: String) {
|
||||||
if let Some(entry) = self.get_local_selected_file() {
|
let entry = if let Some(e) = self.get_selected_file() {
|
||||||
match self
|
e
|
||||||
.host_bridge
|
} else {
|
||||||
.symlink(PathBuf::from(name.as_str()).as_path(), entry.path())
|
return;
|
||||||
{
|
};
|
||||||
Ok(_) => {
|
let link_path = PathBuf::from(name.as_str());
|
||||||
self.log(
|
match self
|
||||||
LogLevel::Info,
|
.browser
|
||||||
format!(
|
.fs_pane_mut()
|
||||||
"Created symlink at {}, pointing to {}",
|
.fs
|
||||||
name,
|
.symlink(link_path.as_path(), entry.path())
|
||||||
entry.path().display()
|
{
|
||||||
),
|
Ok(_) => {
|
||||||
);
|
self.log(
|
||||||
}
|
LogLevel::Info,
|
||||||
Err(err) => {
|
format!(
|
||||||
self.log_and_alert(LogLevel::Error, format!("Could not create symlink: {err}"));
|
"Created symlink at {}, pointing to {}",
|
||||||
}
|
name,
|
||||||
|
entry.path().display()
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
Err(err) => {
|
||||||
}
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
/// Copy file on remote
|
format!(
|
||||||
pub(crate) fn action_remote_symlink(&mut self, name: String) {
|
"Could not create symlink pointing to {}: {}",
|
||||||
if let Some(entry) = self.get_remote_selected_file() {
|
entry.path().display(),
|
||||||
match self
|
err
|
||||||
.client
|
),
|
||||||
.symlink(PathBuf::from(name.as_str()).as_path(), entry.path())
|
);
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!(
|
|
||||||
"Created symlink at {}, pointing to {}",
|
|
||||||
name,
|
|
||||||
entry.path().display()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not create symlink pointing to {}: {}",
|
|
||||||
entry.path().display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -15,17 +15,22 @@ pub enum WalkdirError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_walkdir_local(&mut self) -> Result<Vec<File>, WalkdirError> {
|
/// Walk the directory tree from the current working directory of the active pane.
|
||||||
|
pub(crate) fn action_walkdir(&mut self) -> Result<Vec<File>, WalkdirError> {
|
||||||
let mut acc = Vec::with_capacity(32_768);
|
let mut acc = Vec::with_capacity(32_768);
|
||||||
|
|
||||||
let pwd = self
|
let pwd = self
|
||||||
.host_bridge
|
.browser
|
||||||
|
.fs_pane_mut()
|
||||||
|
.fs
|
||||||
.pwd()
|
.pwd()
|
||||||
.map_err(|e| WalkdirError::Error(e.to_string()))?;
|
.map_err(|e| WalkdirError::Error(e.to_string()))?;
|
||||||
|
|
||||||
self.walkdir(&mut acc, &pwd, |activity, path| {
|
self.walkdir(&mut acc, &pwd, |activity, path| {
|
||||||
activity
|
activity
|
||||||
.host_bridge
|
.browser
|
||||||
|
.fs_pane_mut()
|
||||||
|
.fs
|
||||||
.list_dir(path)
|
.list_dir(path)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
})?;
|
})?;
|
||||||
@@ -33,21 +38,6 @@ impl FileTransferActivity {
|
|||||||
Ok(acc)
|
Ok(acc)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_walkdir_remote(&mut self) -> Result<Vec<File>, WalkdirError> {
|
|
||||||
let mut acc = Vec::with_capacity(32_768);
|
|
||||||
|
|
||||||
let pwd = self
|
|
||||||
.client
|
|
||||||
.pwd()
|
|
||||||
.map_err(|e| WalkdirError::Error(e.to_string()))?;
|
|
||||||
|
|
||||||
self.walkdir(&mut acc, &pwd, |activity, path| {
|
|
||||||
activity.client.list_dir(path).map_err(|e| e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(acc)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walkdir<F>(
|
fn walkdir<F>(
|
||||||
&mut self,
|
&mut self,
|
||||||
acc: &mut Vec<File>,
|
acc: &mut Vec<File>,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use tui_realm_stdlib::Phantom;
|
|||||||
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers};
|
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers};
|
||||||
use tuirealm::{Component, MockComponent, NoUserEvent};
|
use tuirealm::{Component, MockComponent, NoUserEvent};
|
||||||
|
|
||||||
use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
|
use super::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
// -- export
|
// -- export
|
||||||
mod log;
|
mod log;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ use tuirealm::props::{Alignment, AttrValue, Attribute, BorderSides, Borders, Col
|
|||||||
use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout};
|
use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout};
|
||||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue};
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue};
|
||||||
|
|
||||||
use super::{Msg, TransferMsg, UiMsg};
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Item {
|
pub enum Item {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use tui_realm_stdlib::Input;
|
||||||
|
use tuirealm::command::{Cmd, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct CopyPopup {
|
||||||
|
component: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CopyPopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.placeholder(
|
||||||
|
"destination",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
)
|
||||||
|
.title("Copy file(s) to\u{2026}", Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for CopyPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Delete, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Cancel);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Delete);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(ch),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Type(ch));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.state() {
|
||||||
|
State::One(StateValue::String(i)) => {
|
||||||
|
Some(Msg::Transfer(TransferMsg::CopyFileTo(i)))
|
||||||
|
}
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseCopyPopup))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use tui_realm_stdlib::Radio;
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction};
|
||||||
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct DeletePopup {
|
||||||
|
component: Radio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeletePopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.choices(["Yes", "No"])
|
||||||
|
.value(1)
|
||||||
|
.title("Delete file(s)?", Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for DeletePopup {
|
||||||
|
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::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseDeletePopup))
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('y'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Transfer(TransferMsg::DeleteFile)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('n'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::CloseDeletePopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => {
|
||||||
|
if matches!(
|
||||||
|
self.perform(Cmd::Submit),
|
||||||
|
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||||
|
) {
|
||||||
|
Some(Msg::Transfer(TransferMsg::DeleteFile))
|
||||||
|
} else {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseDeletePopup))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use tui_realm_stdlib::Radio;
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction};
|
||||||
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct DisconnectPopup {
|
||||||
|
component: Radio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisconnectPopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.choices(["Yes", "No"])
|
||||||
|
.title("Are you sure you want to disconnect?", Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for DisconnectPopup {
|
||||||
|
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::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseDisconnectPopup))
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('y'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::Disconnect)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('n'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::CloseDisconnectPopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => {
|
||||||
|
if matches!(
|
||||||
|
self.perform(Cmd::Submit),
|
||||||
|
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||||
|
) {
|
||||||
|
Some(Msg::Ui(UiMsg::Disconnect))
|
||||||
|
} else {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseDisconnectPopup))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
use tui_realm_stdlib::Paragraph;
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct ErrorPopup {
|
||||||
|
component: Paragraph,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorPopup {
|
||||||
|
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Paragraph::default()
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.text([TextSpan::from(text.as_ref())])
|
||||||
|
.wrap(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for ErrorPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Esc | Key::Enter,
|
||||||
|
..
|
||||||
|
}) => Some(Msg::Ui(UiMsg::CloseErrorPopup)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct FatalPopup {
|
||||||
|
component: Paragraph,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FatalPopup {
|
||||||
|
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Paragraph::default()
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.text([TextSpan::from(text.as_ref())])
|
||||||
|
.wrap(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for FatalPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Esc | Key::Enter,
|
||||||
|
..
|
||||||
|
}) => Some(Msg::Ui(UiMsg::CloseFatalPopup)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
use std::time::UNIX_EPOCH;
|
||||||
|
|
||||||
|
use bytesize::ByteSize;
|
||||||
|
use remotefs::File;
|
||||||
|
use tui_realm_stdlib::List;
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||||
|
#[cfg(posix)]
|
||||||
|
use uzers::{get_group_by_gid, get_user_by_uid};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, UiMsg};
|
||||||
|
use crate::utils::fmt::fmt_time;
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct FileInfoPopup {
|
||||||
|
component: List,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileInfoPopup {
|
||||||
|
pub fn new(file: &File) -> Self {
|
||||||
|
let mut texts: TableBuilder = TableBuilder::default();
|
||||||
|
// Abs path
|
||||||
|
let real_path = file.metadata().symlink.as_deref();
|
||||||
|
let path: String = match real_path {
|
||||||
|
Some(symlink) => format!("{} -> {}", file.path().display(), symlink.display()),
|
||||||
|
None => format!("{}", file.path().display()),
|
||||||
|
};
|
||||||
|
// Make texts
|
||||||
|
texts
|
||||||
|
.add_col(TextSpan::from("Path: "))
|
||||||
|
.add_col(TextSpan::new(path.as_str()).fg(Color::Yellow));
|
||||||
|
texts
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::from("Name: "))
|
||||||
|
.add_col(TextSpan::new(file.name()).fg(Color::Yellow));
|
||||||
|
if let Some(filetype) = file.extension() {
|
||||||
|
texts
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::from("File type: "))
|
||||||
|
.add_col(TextSpan::new(filetype).fg(Color::LightGreen));
|
||||||
|
}
|
||||||
|
let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size);
|
||||||
|
texts
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::from("Size: "))
|
||||||
|
.add_col(TextSpan::new(format!("{bsize} ({size})").as_str()).fg(Color::Cyan));
|
||||||
|
let atime: String = fmt_time(
|
||||||
|
file.metadata().accessed.unwrap_or(UNIX_EPOCH),
|
||||||
|
"%b %d %Y %H:%M:%S",
|
||||||
|
);
|
||||||
|
let ctime: String = fmt_time(
|
||||||
|
file.metadata().created.unwrap_or(UNIX_EPOCH),
|
||||||
|
"%b %d %Y %H:%M:%S",
|
||||||
|
);
|
||||||
|
let mtime: String = fmt_time(
|
||||||
|
file.metadata().modified.unwrap_or(UNIX_EPOCH),
|
||||||
|
"%b %d %Y %H:%M:%S",
|
||||||
|
);
|
||||||
|
texts
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::from("Creation time: "))
|
||||||
|
.add_col(TextSpan::new(ctime.as_str()).fg(Color::LightGreen));
|
||||||
|
texts
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::from("Last modified time: "))
|
||||||
|
.add_col(TextSpan::new(mtime.as_str()).fg(Color::LightBlue));
|
||||||
|
texts
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::from("Last access time: "))
|
||||||
|
.add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed));
|
||||||
|
// User
|
||||||
|
#[cfg(posix)]
|
||||||
|
let username: String = match file.metadata().uid {
|
||||||
|
Some(uid) => match get_user_by_uid(uid) {
|
||||||
|
Some(user) => user.name().to_string_lossy().to_string(),
|
||||||
|
None => uid.to_string(),
|
||||||
|
},
|
||||||
|
None => String::from("0"),
|
||||||
|
};
|
||||||
|
#[cfg(win)]
|
||||||
|
let username: String = format!("{}", file.metadata().uid.unwrap_or(0));
|
||||||
|
// Group
|
||||||
|
#[cfg(posix)]
|
||||||
|
let group: String = match file.metadata().gid {
|
||||||
|
Some(gid) => match get_group_by_gid(gid) {
|
||||||
|
Some(group) => group.name().to_string_lossy().to_string(),
|
||||||
|
None => gid.to_string(),
|
||||||
|
},
|
||||||
|
None => String::from("0"),
|
||||||
|
};
|
||||||
|
#[cfg(win)]
|
||||||
|
let group: String = format!("{}", file.metadata().gid.unwrap_or(0));
|
||||||
|
texts
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::from("User: "))
|
||||||
|
.add_col(TextSpan::new(username.as_str()).fg(Color::LightYellow));
|
||||||
|
texts
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::from("Group: "))
|
||||||
|
.add_col(TextSpan::new(group.as_str()).fg(Color::Blue));
|
||||||
|
Self {
|
||||||
|
component: List::default()
|
||||||
|
.borders(Borders::default().modifiers(BorderType::Rounded))
|
||||||
|
.scroll(false)
|
||||||
|
.title(file.name(), Alignment::Left)
|
||||||
|
.rows(texts.build()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for FileInfoPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Esc | Key::Enter,
|
||||||
|
..
|
||||||
|
}) => Some(Msg::Ui(UiMsg::CloseFileInfoPopup)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use tui_realm_stdlib::Input;
|
||||||
|
use tuirealm::command::{Cmd, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct FilterPopup {
|
||||||
|
component: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterPopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.placeholder(
|
||||||
|
"regex or wildmatch",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
)
|
||||||
|
.title(
|
||||||
|
"Filter files by regex or wildmatch in the current directory",
|
||||||
|
Alignment::Center,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for FilterPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Delete, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Cancel);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Delete);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(ch),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Type(ch));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.state() {
|
||||||
|
State::One(StateValue::String(filter)) => Some(Msg::Ui(UiMsg::FilterFiles(filter))),
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseFilterPopup))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
use tui_realm_stdlib::List;
|
||||||
|
use tuirealm::command::{Cmd, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct KeybindingsPopup {
|
||||||
|
component: List,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeybindingsPopup {
|
||||||
|
pub fn new(key_color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: List::default()
|
||||||
|
.borders(Borders::default().modifiers(BorderType::Rounded))
|
||||||
|
.scroll(true)
|
||||||
|
.step(8)
|
||||||
|
.highlighted_str("? ")
|
||||||
|
.title("Keybindings", Alignment::Center)
|
||||||
|
.rewind(true)
|
||||||
|
.rows(
|
||||||
|
TableBuilder::default()
|
||||||
|
.add_col(TextSpan::new("<ESC>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Disconnect"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<BACKSPACE>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Go to previous directory"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<TAB|RIGHT|LEFT>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Change explorer tab"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Move up/down in list"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<ENTER>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Enter directory"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<SPACE>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Upload/Download file"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<BACKTAB>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(
|
||||||
|
" Switch between explorer and log window",
|
||||||
|
))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<A>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Toggle hidden files"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<B>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Change file sorting mode"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<C|F5>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Copy"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<D|F7>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Make directory"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<F>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Search files"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<G>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Go to path"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<H|F1>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Show help"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<I>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(
|
||||||
|
" Show info about selected file",
|
||||||
|
))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<K>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(
|
||||||
|
" Create symlink pointing to the current selected entry",
|
||||||
|
))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<L>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Reload directory content"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<M>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Select file"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<N>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Create new file"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<O|F4>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(
|
||||||
|
" Open text file with preferred editor",
|
||||||
|
))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<P>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Toggle bottom panel"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<Q|F10>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Quit termscp"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<R|F6>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Rename file"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<S|F2>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Save file as"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<T>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Watch/unwatch file changes"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<U>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Go to parent directory"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<V|F3>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(
|
||||||
|
" Open file with default application for file type",
|
||||||
|
))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<W>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(
|
||||||
|
" Open file with specified application",
|
||||||
|
))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<X>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Execute shell command"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<Y>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(
|
||||||
|
" Toggle synchronized browsing",
|
||||||
|
))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<Z>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Change file permissions"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("</>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Filter files"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<DEL|F8|E>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Delete selected file"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<CTRL+A>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Select all files"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<ALT+A>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Deselect all files"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Interrupt file transfer"))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<CTRL+S>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(
|
||||||
|
" Get total path size of selected files",
|
||||||
|
))
|
||||||
|
.add_row()
|
||||||
|
.add_col(TextSpan::new("<CTRL+T>").bold().fg(key_color))
|
||||||
|
.add_col(TextSpan::from(" Show watched paths"))
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for KeybindingsPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Esc | Key::Enter,
|
||||||
|
..
|
||||||
|
}) => Some(Msg::Ui(UiMsg::CloseKeybindingsPopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Down, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Down));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Up));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::PageDown,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Scroll(Direction::Down));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::PageUp, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Scroll(Direction::Up));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use tui_realm_stdlib::{Input, Radio};
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, PendingActionMsg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct MkdirPopup {
|
||||||
|
component: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MkdirPopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.placeholder(
|
||||||
|
"New directory name",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
)
|
||||||
|
.title("directory-name", Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for MkdirPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Delete, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Cancel);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Delete);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(ch),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Type(ch));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.state() {
|
||||||
|
State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::Mkdir(i))),
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseMkdirPopup))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct SyncBrowsingMkdirPopup {
|
||||||
|
component: Radio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncBrowsingMkdirPopup {
|
||||||
|
pub fn new(color: Color, dir_name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.choices(["Yes", "No"])
|
||||||
|
.title(
|
||||||
|
format!(
|
||||||
|
r#"Sync browsing: directory "{dir_name}" doesn't exist. Do you want to create it?"#
|
||||||
|
),
|
||||||
|
Alignment::Center,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for SyncBrowsingMkdirPopup {
|
||||||
|
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::Esc, .. }) => Some(Msg::PendingAction(
|
||||||
|
PendingActionMsg::CloseSyncBrowsingMkdirPopup,
|
||||||
|
)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('y'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('n'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::PendingAction(
|
||||||
|
PendingActionMsg::CloseSyncBrowsingMkdirPopup,
|
||||||
|
)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => {
|
||||||
|
if matches!(
|
||||||
|
self.perform(Cmd::Submit),
|
||||||
|
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||||
|
) {
|
||||||
|
Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory))
|
||||||
|
} else {
|
||||||
|
Some(Msg::PendingAction(
|
||||||
|
PendingActionMsg::CloseSyncBrowsingMkdirPopup,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use tui_realm_stdlib::Input;
|
||||||
|
use tuirealm::command::{Cmd, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct NewfilePopup {
|
||||||
|
component: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewfilePopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.placeholder(
|
||||||
|
"New file name",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
)
|
||||||
|
.title("file.txt", Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for NewfilePopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Delete, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Cancel);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Delete);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(ch),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Type(ch));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.state() {
|
||||||
|
State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::NewFile(i))),
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseNewFilePopup))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use tui_realm_stdlib::Input;
|
||||||
|
use tuirealm::command::{Cmd, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct OpenWithPopup {
|
||||||
|
component: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenWithPopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.placeholder(
|
||||||
|
"Open file with\u{2026}",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
)
|
||||||
|
.title("Type the program to open the file with", Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for OpenWithPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Delete, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Cancel);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Delete);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(ch),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Type(ch));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.state() {
|
||||||
|
State::One(StateValue::String(i)) => {
|
||||||
|
Some(Msg::Transfer(TransferMsg::OpenFileWith(i)))
|
||||||
|
}
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseOpenWithPopup))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use tui_realm_stdlib::ProgressBar;
|
||||||
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
|
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct ProgressBarFull {
|
||||||
|
component: ProgressBar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressBarFull {
|
||||||
|
pub fn new<S: Into<String>>(prog: f64, label: S, title: S, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: ProgressBar::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.modifiers(BorderType::Rounded)
|
||||||
|
.sides(BorderSides::TOP | BorderSides::LEFT | BorderSides::RIGHT),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.label(label)
|
||||||
|
.progress(prog)
|
||||||
|
.title(title, Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for ProgressBarFull {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
if matches!(
|
||||||
|
ev,
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('c'),
|
||||||
|
modifiers: KeyModifiers::CONTROL
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
Some(Msg::Transfer(TransferMsg::AbortTransfer))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct ProgressBarPartial {
|
||||||
|
component: ProgressBar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressBarPartial {
|
||||||
|
pub fn new<S: Into<String>>(prog: f64, label: S, title: S, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: ProgressBar::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.modifiers(BorderType::Rounded)
|
||||||
|
.sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.label(label)
|
||||||
|
.progress(prog)
|
||||||
|
.title(title, Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for ProgressBarPartial {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
if matches!(
|
||||||
|
ev,
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('c'),
|
||||||
|
modifiers: KeyModifiers::CONTROL
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
Some(Msg::Transfer(TransferMsg::AbortTransfer))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use tui_realm_stdlib::Radio;
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction};
|
||||||
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct QuitPopup {
|
||||||
|
component: Radio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuitPopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.choices(["Yes", "No"])
|
||||||
|
.title("Are you sure you want to quit termscp?", Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for QuitPopup {
|
||||||
|
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::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseQuitPopup))
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('y'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::Quit)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('n'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::CloseQuitPopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => {
|
||||||
|
if matches!(
|
||||||
|
self.perform(Cmd::Submit),
|
||||||
|
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||||
|
) {
|
||||||
|
Some(Msg::Ui(UiMsg::Quit))
|
||||||
|
} else {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseQuitPopup))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use tui_realm_stdlib::Input;
|
||||||
|
use tuirealm::command::{Cmd, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct RenamePopup {
|
||||||
|
component: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenamePopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.placeholder(
|
||||||
|
"/foo/bar/buzz.txt",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
)
|
||||||
|
.title("Move file(s) to\u{2026}", Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for RenamePopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Delete, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Cancel);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Delete);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(ch),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Type(ch));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.state() {
|
||||||
|
State::One(StateValue::String(i)) => {
|
||||||
|
Some(Msg::Transfer(TransferMsg::RenameFile(i)))
|
||||||
|
}
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseRenamePopup))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use tui_realm_stdlib::Radio;
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction};
|
||||||
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, PendingActionMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct ReplacePopup {
|
||||||
|
component: Radio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReplacePopup {
|
||||||
|
pub fn new(filename: Option<&str>, color: Color) -> Self {
|
||||||
|
let text = match filename {
|
||||||
|
Some(f) => format!(r#"File "{f}" already exists. Overwrite file?"#),
|
||||||
|
None => "Overwrite files?".to_string(),
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.choices(["Replace", "Skip", "Replace All", "Skip All", "Cancel"])
|
||||||
|
.title(text, Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for ReplacePopup {
|
||||||
|
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::Esc, .. }) => {
|
||||||
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('y'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('n'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.perform(Cmd::Submit) {
|
||||||
|
CmdResult::Submit(State::One(StateValue::Usize(0))) => {
|
||||||
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite))
|
||||||
|
}
|
||||||
|
CmdResult::Submit(State::One(StateValue::Usize(1))) => {
|
||||||
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip))
|
||||||
|
}
|
||||||
|
CmdResult::Submit(State::One(StateValue::Usize(2))) => {
|
||||||
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll))
|
||||||
|
}
|
||||||
|
CmdResult::Submit(State::One(StateValue::Usize(3))) => {
|
||||||
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkipAll))
|
||||||
|
}
|
||||||
|
CmdResult::Submit(State::One(StateValue::Usize(4))) => {
|
||||||
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
|
||||||
|
}
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use tui_realm_stdlib::Input;
|
||||||
|
use tuirealm::command::{Cmd, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct SaveAsPopup {
|
||||||
|
component: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SaveAsPopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.placeholder(
|
||||||
|
"/foo/bar/buzz.txt",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
)
|
||||||
|
.title("Save as\u{2026}", Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for SaveAsPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Delete, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Cancel);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Delete);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(ch),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Type(ch));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.state() {
|
||||||
|
State::One(StateValue::String(i)) => {
|
||||||
|
Some(Msg::Transfer(TransferMsg::SaveFileAs(i)))
|
||||||
|
}
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseSaveAsPopup))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use tui_realm_stdlib::Radio;
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::explorer::FileSorting;
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct SortingPopup {
|
||||||
|
component: Radio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SortingPopup {
|
||||||
|
pub fn new(value: FileSorting, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.choices(["Name", "Modify time", "Creation time", "Size"])
|
||||||
|
.title("Sort files by\u{2026}", Alignment::Center)
|
||||||
|
.value(match value {
|
||||||
|
FileSorting::CreationTime => 2,
|
||||||
|
FileSorting::ModifyTime => 1,
|
||||||
|
FileSorting::Name => 0,
|
||||||
|
FileSorting::Size => 3,
|
||||||
|
FileSorting::None => 0,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for SortingPopup {
|
||||||
|
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::Esc | Key::Enter,
|
||||||
|
..
|
||||||
|
}) => return Some(Msg::Ui(UiMsg::CloseFileSortingPopup)),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
if let CmdResult::Changed(State::One(StateValue::Usize(i))) = result {
|
||||||
|
Some(Msg::Ui(UiMsg::ChangeFileSorting(match i {
|
||||||
|
0 => FileSorting::Name,
|
||||||
|
1 => FileSorting::ModifyTime,
|
||||||
|
2 => FileSorting::CreationTime,
|
||||||
|
3 => FileSorting::Size,
|
||||||
|
_ => FileSorting::Name,
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
use tui_realm_stdlib::Span;
|
||||||
|
use tuirealm::props::{Color, TextSpan};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||||
|
|
||||||
|
use crate::explorer::FileSorting;
|
||||||
|
use crate::ui::activities::filetransfer::Msg;
|
||||||
|
use crate::ui::activities::filetransfer::lib::browser::Browser;
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct StatusBarLocal {
|
||||||
|
component: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusBarLocal {
|
||||||
|
pub fn new(browser: &Browser, sorting_color: Color, hidden_color: Color) -> Self {
|
||||||
|
let file_sorting = file_sorting_label(browser.host_bridge().file_sorting);
|
||||||
|
let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible());
|
||||||
|
Self {
|
||||||
|
component: Span::default().spans([
|
||||||
|
TextSpan::new("File sorting: ").fg(sorting_color),
|
||||||
|
TextSpan::new(file_sorting).fg(sorting_color).reversed(),
|
||||||
|
TextSpan::new(" Hidden files: ").fg(hidden_color),
|
||||||
|
TextSpan::new(hidden_files).fg(hidden_color).reversed(),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for StatusBarLocal {
|
||||||
|
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct StatusBarRemote {
|
||||||
|
component: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusBarRemote {
|
||||||
|
pub fn new(
|
||||||
|
browser: &Browser,
|
||||||
|
sorting_color: Color,
|
||||||
|
hidden_color: Color,
|
||||||
|
sync_color: Color,
|
||||||
|
) -> Self {
|
||||||
|
let file_sorting = file_sorting_label(browser.remote().file_sorting);
|
||||||
|
let hidden_files = hidden_files_label(browser.remote().hidden_files_visible());
|
||||||
|
let sync_browsing = match browser.sync_browsing {
|
||||||
|
true => "ON ",
|
||||||
|
false => "OFF",
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
component: Span::default().spans([
|
||||||
|
TextSpan::new("File sorting: ").fg(sorting_color),
|
||||||
|
TextSpan::new(file_sorting).fg(sorting_color).reversed(),
|
||||||
|
TextSpan::new(" Hidden files: ").fg(hidden_color),
|
||||||
|
TextSpan::new(hidden_files).fg(hidden_color).reversed(),
|
||||||
|
TextSpan::new(" Sync browsing: ").fg(sync_color),
|
||||||
|
TextSpan::new(sync_browsing).fg(sync_color).reversed(),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for StatusBarRemote {
|
||||||
|
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_sorting_label(sorting: FileSorting) -> &'static str {
|
||||||
|
match sorting {
|
||||||
|
FileSorting::CreationTime => "By creation time",
|
||||||
|
FileSorting::ModifyTime => "By modify time",
|
||||||
|
FileSorting::Name => "By name",
|
||||||
|
FileSorting::Size => "By size",
|
||||||
|
FileSorting::None => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hidden_files_label(visible: bool) -> &'static str {
|
||||||
|
match visible {
|
||||||
|
true => "Show",
|
||||||
|
false => "Hide",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use tui_realm_stdlib::Input;
|
||||||
|
use tuirealm::command::{Cmd, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct SymlinkPopup {
|
||||||
|
component: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymlinkPopup {
|
||||||
|
pub fn new(color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Input::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.input_type(InputType::Text)
|
||||||
|
.placeholder(
|
||||||
|
"Symlink name",
|
||||||
|
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||||
|
)
|
||||||
|
.title(
|
||||||
|
"Create a symlink pointing to the selected entry",
|
||||||
|
Alignment::Center,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for SymlinkPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Left));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Right, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Right));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Delete, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Cancel);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Delete);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(ch),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Type(ch));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => match self.state() {
|
||||||
|
State::One(StateValue::String(i)) => {
|
||||||
|
Some(Msg::Transfer(TransferMsg::CreateSymlink(i)))
|
||||||
|
}
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseSymlinkPopup))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
use tui_realm_stdlib::Paragraph;
|
||||||
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct WaitPopup {
|
||||||
|
component: Paragraph,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaitPopup {
|
||||||
|
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Paragraph::default()
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.text([TextSpan::from(text.as_ref())])
|
||||||
|
.wrap(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for WaitPopup {
|
||||||
|
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct WalkdirWaitPopup {
|
||||||
|
component: Paragraph,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalkdirWaitPopup {
|
||||||
|
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: Paragraph::default()
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.text([
|
||||||
|
TextSpan::from(text.as_ref()),
|
||||||
|
TextSpan::from("Press 'CTRL+C' to abort"),
|
||||||
|
])
|
||||||
|
.wrap(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for WalkdirWaitPopup {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
if matches!(
|
||||||
|
ev,
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('c'),
|
||||||
|
modifiers: KeyModifiers::CONTROL
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
Some(Msg::Transfer(TransferMsg::AbortWalkdir))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
use tui_realm_stdlib::{List, Radio};
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
|
use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan};
|
||||||
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct WatchedPathsList {
|
||||||
|
component: List,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WatchedPathsList {
|
||||||
|
pub fn new(paths: &[std::path::PathBuf], color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: List::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.rewind(true)
|
||||||
|
.scroll(true)
|
||||||
|
.step(4)
|
||||||
|
.highlighted_color(color)
|
||||||
|
.highlighted_str("\u{27a4} ")
|
||||||
|
.title(
|
||||||
|
"These files are currently synched with the remote host",
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.rows(
|
||||||
|
paths
|
||||||
|
.iter()
|
||||||
|
.map(|x| vec![TextSpan::from(x.to_string_lossy().to_string())])
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for WatchedPathsList {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match ev {
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseWatchedPathsList))
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Down, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Down));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
|
self.perform(Cmd::Move(Direction::Up));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::PageDown,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Scroll(Direction::Down));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::PageUp, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Scroll(Direction::Up));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Home, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::Begin));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||||
|
self.perform(Cmd::GoTo(Position::End));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => {
|
||||||
|
// get state
|
||||||
|
if let State::One(StateValue::Usize(idx)) = self.component.state() {
|
||||||
|
Some(Msg::Transfer(TransferMsg::ToggleWatchFor(idx)))
|
||||||
|
} else {
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct WatcherPopup {
|
||||||
|
component: Radio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WatcherPopup {
|
||||||
|
pub fn new(watched: bool, local: &str, remote: &str, color: Color) -> Self {
|
||||||
|
let text = match watched {
|
||||||
|
false => format!(r#"Synchronize changes from "{local}" to "{remote}"?"#),
|
||||||
|
true => format!(r#"Stop synchronizing changes at "{local}"?"#),
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
component: Radio::default()
|
||||||
|
.borders(
|
||||||
|
Borders::default()
|
||||||
|
.color(color)
|
||||||
|
.modifiers(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.foreground(color)
|
||||||
|
.choices(["Yes", "No"])
|
||||||
|
.title(text, Alignment::Center),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for WatcherPopup {
|
||||||
|
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::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseWatcherPopup))
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('y'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Transfer(TransferMsg::ToggleWatch)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('n'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::CloseWatcherPopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => {
|
||||||
|
if matches!(
|
||||||
|
self.perform(Cmd::Submit),
|
||||||
|
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||||
|
) {
|
||||||
|
Some(Msg::Transfer(TransferMsg::ToggleWatch))
|
||||||
|
} else {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseWatcherPopup))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,54 @@ impl TerminalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MockComponent for TerminalComponent {
|
impl MockComponent for TerminalComponent {
|
||||||
|
fn view(&mut self, frame: &mut tuirealm::Frame, area: Rect) {
|
||||||
|
let width = area.width.saturating_sub(2);
|
||||||
|
let height = area.height.saturating_sub(2);
|
||||||
|
|
||||||
|
// update the terminal size if it has changed
|
||||||
|
if self.size != (width, height) {
|
||||||
|
self.size = (width, height);
|
||||||
|
self.parser.set_size(height, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = self
|
||||||
|
.query(Attribute::Title)
|
||||||
|
.map(|value| value.unwrap_string())
|
||||||
|
.unwrap_or_else(|| "Terminal".to_string());
|
||||||
|
|
||||||
|
let fg = self
|
||||||
|
.query(Attribute::Foreground)
|
||||||
|
.map(|value| value.unwrap_color())
|
||||||
|
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
||||||
|
|
||||||
|
let bg = self
|
||||||
|
.query(Attribute::Background)
|
||||||
|
.map(|value| value.unwrap_color())
|
||||||
|
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
||||||
|
|
||||||
|
let border_color = self
|
||||||
|
.query(Attribute::Borders)
|
||||||
|
.map(|value| value.unwrap_color())
|
||||||
|
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
||||||
|
|
||||||
|
let terminal = PseudoTerminal::new(self.parser.screen())
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(title)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(border_color))
|
||||||
|
.borders(BorderSides::ALL)
|
||||||
|
.style(Style::default().fg(fg).bg(bg)),
|
||||||
|
)
|
||||||
|
.style(Style::default().fg(fg).bg(bg));
|
||||||
|
|
||||||
|
frame.render_widget(terminal, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query(&self, attr: tuirealm::Attribute) -> Option<tuirealm::AttrValue> {
|
||||||
|
self.props.get(attr)
|
||||||
|
}
|
||||||
|
|
||||||
fn attr(&mut self, attr: tuirealm::Attribute, value: AttrValue) {
|
fn attr(&mut self, attr: tuirealm::Attribute, value: AttrValue) {
|
||||||
if attr == Attribute::Text {
|
if attr == Attribute::Text {
|
||||||
if let tuirealm::AttrValue::String(s) = value {
|
if let tuirealm::AttrValue::String(s) = value {
|
||||||
@@ -97,6 +145,10 @@ impl MockComponent for TerminalComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::One(StateValue::String(self.line.content().to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||||
match cmd {
|
match cmd {
|
||||||
Cmd::Type(s) => {
|
Cmd::Type(s) => {
|
||||||
@@ -234,56 +286,4 @@ impl MockComponent for TerminalComponent {
|
|||||||
_ => CmdResult::None,
|
_ => CmdResult::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query(&self, attr: tuirealm::Attribute) -> Option<tuirealm::AttrValue> {
|
|
||||||
self.props.get(attr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> State {
|
|
||||||
State::One(StateValue::String(self.line.content().to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&mut self, frame: &mut tuirealm::Frame, area: Rect) {
|
|
||||||
let width = area.width.saturating_sub(2);
|
|
||||||
let height = area.height.saturating_sub(2);
|
|
||||||
|
|
||||||
// update the terminal size if it has changed
|
|
||||||
if self.size != (width, height) {
|
|
||||||
self.size = (width, height);
|
|
||||||
self.parser.set_size(height, width);
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = self
|
|
||||||
.query(Attribute::Title)
|
|
||||||
.map(|value| value.unwrap_string())
|
|
||||||
.unwrap_or_else(|| "Terminal".to_string());
|
|
||||||
|
|
||||||
let fg = self
|
|
||||||
.query(Attribute::Foreground)
|
|
||||||
.map(|value| value.unwrap_color())
|
|
||||||
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
|
||||||
|
|
||||||
let bg = self
|
|
||||||
.query(Attribute::Background)
|
|
||||||
.map(|value| value.unwrap_color())
|
|
||||||
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
|
||||||
|
|
||||||
let border_color = self
|
|
||||||
.query(Attribute::Borders)
|
|
||||||
.map(|value| value.unwrap_color())
|
|
||||||
.unwrap_or(tuirealm::ratatui::style::Color::Reset);
|
|
||||||
|
|
||||||
let terminal = PseudoTerminal::new(self.parser.screen())
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.title(title)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(Style::default().fg(border_color))
|
|
||||||
.borders(BorderSides::ALL)
|
|
||||||
.style(Style::default().fg(fg).bg(bg)),
|
|
||||||
)
|
|
||||||
.style(Style::default().fg(fg).bg(bg));
|
|
||||||
|
|
||||||
frame.render_widget(terminal, area);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ impl FileTransferActivity {
|
|||||||
destination.display()
|
destination.display()
|
||||||
);
|
);
|
||||||
// stat fs entry
|
// stat fs entry
|
||||||
let origin = match self.client.stat(source) {
|
let origin = match self.browser.remote_pane_mut().fs.stat(source) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log(
|
self.log(
|
||||||
@@ -71,7 +71,18 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_watched_file(&mut self, file: &Path) {
|
fn remove_watched_file(&mut self, file: &Path) {
|
||||||
match self.client.remove_dir_all(file) {
|
// stat the file first to use HostBridge::remove
|
||||||
|
let entry = match self.browser.remote_pane_mut().fs.stat(file) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(err) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("failed to stat watched file {}: {}", file.display(), err),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match self.browser.remote_pane_mut().fs.remove(&entry) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
@@ -89,7 +100,7 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
fn upload_watched_file(&mut self, host: &Path, remote: &Path) {
|
fn upload_watched_file(&mut self, host: &Path, remote: &Path) {
|
||||||
// stat host file
|
// stat host file
|
||||||
let entry = match self.host_bridge.stat(host) {
|
let entry = match self.browser.local_pane_mut().fs.stat(host) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log(
|
self.log(
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use nucleo::Utf32String;
|
use nucleo::Utf32String;
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
|
|
||||||
|
use super::pane::Pane;
|
||||||
use crate::explorer::builder::FileExplorerBuilder;
|
use crate::explorer::builder::FileExplorerBuilder;
|
||||||
use crate::explorer::{FileExplorer, FileSorting};
|
use crate::explorer::{FileExplorer, FileSorting};
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
@@ -31,19 +32,19 @@ pub enum FoundExplorerTab {
|
|||||||
|
|
||||||
/// Browser contains the browser options
|
/// Browser contains the browser options
|
||||||
pub struct Browser {
|
pub struct Browser {
|
||||||
host_bridge: FileExplorer, // Local File explorer state
|
local: Pane,
|
||||||
remote: FileExplorer, // Remote File explorer state
|
remote: Pane,
|
||||||
found: Option<Found>, // File explorer for find result
|
found: Option<Found>, // File explorer for find result
|
||||||
tab: FileExplorerTab, // Current selected tab
|
tab: FileExplorerTab, // Current selected tab
|
||||||
pub sync_browsing: bool,
|
pub sync_browsing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Browser {
|
impl Browser {
|
||||||
/// Build a new `Browser` struct
|
/// Build a new `Browser` struct
|
||||||
pub fn new(cli: &ConfigClient) -> Self {
|
pub fn new(local: Pane, remote: Pane) -> Self {
|
||||||
Self {
|
Self {
|
||||||
host_bridge: Self::build_local_explorer(cli),
|
local,
|
||||||
remote: Self::build_remote_explorer(cli),
|
remote,
|
||||||
found: None,
|
found: None,
|
||||||
tab: FileExplorerTab::HostBridge,
|
tab: FileExplorerTab::HostBridge,
|
||||||
sync_browsing: false,
|
sync_browsing: false,
|
||||||
@@ -52,8 +53,8 @@ impl Browser {
|
|||||||
|
|
||||||
pub fn explorer(&self) -> &FileExplorer {
|
pub fn explorer(&self) -> &FileExplorer {
|
||||||
match self.tab {
|
match self.tab {
|
||||||
FileExplorerTab::HostBridge => &self.host_bridge,
|
FileExplorerTab::HostBridge => &self.local.explorer,
|
||||||
FileExplorerTab::Remote => &self.remote,
|
FileExplorerTab::Remote => &self.remote.explorer,
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||||
self.found.as_ref().map(|x| &x.explorer).unwrap()
|
self.found.as_ref().map(|x| &x.explorer).unwrap()
|
||||||
}
|
}
|
||||||
@@ -62,15 +63,15 @@ impl Browser {
|
|||||||
|
|
||||||
pub fn other_explorer_no_found(&self) -> &FileExplorer {
|
pub fn other_explorer_no_found(&self) -> &FileExplorer {
|
||||||
match self.tab {
|
match self.tab {
|
||||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.remote,
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.remote.explorer,
|
||||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.host_bridge,
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.local.explorer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn explorer_mut(&mut self) -> &mut FileExplorer {
|
pub fn explorer_mut(&mut self) -> &mut FileExplorer {
|
||||||
match self.tab {
|
match self.tab {
|
||||||
FileExplorerTab::HostBridge => &mut self.host_bridge,
|
FileExplorerTab::HostBridge => &mut self.local.explorer,
|
||||||
FileExplorerTab::Remote => &mut self.remote,
|
FileExplorerTab::Remote => &mut self.remote.explorer,
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||||
self.found.as_mut().map(|x| &mut x.explorer).unwrap()
|
self.found.as_mut().map(|x| &mut x.explorer).unwrap()
|
||||||
}
|
}
|
||||||
@@ -78,19 +79,19 @@ impl Browser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn host_bridge(&self) -> &FileExplorer {
|
pub fn host_bridge(&self) -> &FileExplorer {
|
||||||
&self.host_bridge
|
&self.local.explorer
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn host_bridge_mut(&mut self) -> &mut FileExplorer {
|
pub fn host_bridge_mut(&mut self) -> &mut FileExplorer {
|
||||||
&mut self.host_bridge
|
&mut self.local.explorer
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remote(&self) -> &FileExplorer {
|
pub fn remote(&self) -> &FileExplorer {
|
||||||
&self.remote
|
&self.remote.explorer
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remote_mut(&mut self) -> &mut FileExplorer {
|
pub fn remote_mut(&mut self) -> &mut FileExplorer {
|
||||||
&mut self.remote
|
&mut self.remote.explorer
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn found(&self) -> Option<&FileExplorer> {
|
pub fn found(&self) -> Option<&FileExplorer> {
|
||||||
@@ -151,22 +152,70 @@ impl Browser {
|
|||||||
/// Toggle terminal for the current tab
|
/// Toggle terminal for the current tab
|
||||||
pub fn toggle_terminal(&mut self, terminal: bool) {
|
pub fn toggle_terminal(&mut self, terminal: bool) {
|
||||||
if self.tab == FileExplorerTab::HostBridge {
|
if self.tab == FileExplorerTab::HostBridge {
|
||||||
self.host_bridge.toggle_terminal(terminal);
|
self.local.explorer.toggle_terminal(terminal);
|
||||||
} else if self.tab == FileExplorerTab::Remote {
|
} else if self.tab == FileExplorerTab::Remote {
|
||||||
self.remote.toggle_terminal(terminal);
|
self.remote.explorer.toggle_terminal(terminal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if terminal is open for the host bridge tab
|
/// Check if terminal is open for the host bridge tab
|
||||||
pub fn is_terminal_open_host_bridge(&self) -> bool {
|
pub fn is_terminal_open_host_bridge(&self) -> bool {
|
||||||
self.tab == FileExplorerTab::HostBridge && self.host_bridge.terminal_open()
|
self.tab == FileExplorerTab::HostBridge && self.local.explorer.terminal_open()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if terminal is open for the remote tab
|
/// Check if terminal is open for the remote tab
|
||||||
pub fn is_terminal_open_remote(&self) -> bool {
|
pub fn is_terminal_open_remote(&self) -> bool {
|
||||||
self.tab == FileExplorerTab::Remote && self.remote.terminal_open()
|
self.tab == FileExplorerTab::Remote && self.remote.explorer.terminal_open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Pane accessors --
|
||||||
|
|
||||||
|
/// The pane whose filesystem is targeted by the current tab.
|
||||||
|
pub fn fs_pane(&self) -> &Pane {
|
||||||
|
match self.tab {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.local,
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.remote,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pane whose filesystem is targeted by the current tab (mutable).
|
||||||
|
pub fn fs_pane_mut(&mut self) -> &mut Pane {
|
||||||
|
match self.tab {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &mut self.local,
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => &mut self.remote,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The opposite pane (transfer destination).
|
||||||
|
pub fn opposite_pane(&self) -> &Pane {
|
||||||
|
match self.tab {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.remote,
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.local,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Direct access to local pane
|
||||||
|
pub fn local_pane(&self) -> &Pane {
|
||||||
|
&self.local
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Direct access to local pane (mutable)
|
||||||
|
pub fn local_pane_mut(&mut self) -> &mut Pane {
|
||||||
|
&mut self.local
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Direct access to remote pane
|
||||||
|
pub fn remote_pane(&self) -> &Pane {
|
||||||
|
&self.remote
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Direct access to remote pane (mutable)
|
||||||
|
pub fn remote_pane_mut(&mut self) -> &mut Pane {
|
||||||
|
&mut self.remote
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Explorer builders (static helpers) --
|
||||||
|
|
||||||
/// Build a file explorer with local host setup
|
/// Build a file explorer with local host setup
|
||||||
pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer {
|
pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer {
|
||||||
let mut builder = Self::build_explorer(cli);
|
let mut builder = Self::build_explorer(cli);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
pub(crate) mod browser;
|
pub(crate) mod browser;
|
||||||
|
pub(crate) mod pane;
|
||||||
pub(crate) mod transfer;
|
pub(crate) mod transfer;
|
||||||
pub(crate) mod walkdir;
|
pub(crate) mod walkdir;
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
use crate::explorer::FileExplorer;
|
||||||
|
use crate::host::HostBridge;
|
||||||
|
|
||||||
|
/// One side of the dual-pane file browser.
|
||||||
|
///
|
||||||
|
/// Holds the file explorer state, connection tracking, and the filesystem
|
||||||
|
/// client (`HostBridge`) used to interact with the underlying storage
|
||||||
|
/// (local or remote).
|
||||||
|
pub struct Pane {
|
||||||
|
/// File explorer state (directory listing, sorting, filtering, transfer queue)
|
||||||
|
pub(crate) explorer: FileExplorer,
|
||||||
|
/// Whether this pane has been connected at least once
|
||||||
|
pub(crate) connected: bool,
|
||||||
|
/// Filesystem client for this pane
|
||||||
|
pub(crate) fs: Box<dyn HostBridge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pane {
|
||||||
|
/// Create a new Pane.
|
||||||
|
pub fn new(explorer: FileExplorer, connected: bool, fs: Box<dyn HostBridge>) -> Self {
|
||||||
|
Self {
|
||||||
|
explorer,
|
||||||
|
connected,
|
||||||
|
fs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::explorer::builder::FileExplorerBuilder;
|
||||||
|
use crate::host::Localhost;
|
||||||
|
|
||||||
|
fn make_pane() -> Pane {
|
||||||
|
let wrkdir = std::env::temp_dir();
|
||||||
|
let explorer = FileExplorerBuilder::new().build();
|
||||||
|
let fs = Localhost::new(wrkdir).unwrap();
|
||||||
|
Pane::new(explorer, false, Box::new(fs))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pane_new() {
|
||||||
|
let pane = make_pane();
|
||||||
|
assert!(!pane.connected);
|
||||||
|
assert!(pane.fs.is_localhost());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pane_pwd() {
|
||||||
|
let mut pane = make_pane();
|
||||||
|
let pwd = pane.fs.pwd().unwrap();
|
||||||
|
assert_eq!(pwd, PathBuf::from(std::env::temp_dir()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|||||||
@@ -1,22 +1,11 @@
|
|||||||
use std::env;
|
mod filelist;
|
||||||
use std::path::{Path, PathBuf};
|
mod host;
|
||||||
|
mod log;
|
||||||
|
mod notify;
|
||||||
|
|
||||||
use bytesize::ByteSize;
|
|
||||||
use tuirealm::props::{
|
|
||||||
Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextModifiers,
|
|
||||||
TextSpan,
|
|
||||||
};
|
|
||||||
use tuirealm::{PollStrategy, Update};
|
use tuirealm::{PollStrategy, Update};
|
||||||
|
|
||||||
use super::browser::FileExplorerTab;
|
use super::FileTransferActivity;
|
||||||
use super::{ConfigClient, FileTransferActivity, Id, LogLevel, LogRecord, TransferPayload};
|
|
||||||
use crate::filetransfer::{HostBridgeParams, ProtocolParams};
|
|
||||||
use crate::system::environment;
|
|
||||||
use crate::system::notifications::Notification;
|
|
||||||
use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex};
|
|
||||||
use crate::utils::path;
|
|
||||||
|
|
||||||
const LOG_CAPACITY: usize = 256;
|
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// Call `Application::tick()` and process messages in `Update`
|
/// Call `Application::tick()` and process messages in `Update`
|
||||||
@@ -38,563 +27,4 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add message to log events
|
|
||||||
pub(super) fn log(&mut self, level: LogLevel, msg: String) {
|
|
||||||
// Log to file
|
|
||||||
match level {
|
|
||||||
LogLevel::Error => error!("{}", msg),
|
|
||||||
LogLevel::Info => info!("{}", msg),
|
|
||||||
LogLevel::Warn => warn!("{}", msg),
|
|
||||||
}
|
|
||||||
// Create log record
|
|
||||||
let record: LogRecord = LogRecord::new(level, msg);
|
|
||||||
//Check if history overflows the size
|
|
||||||
if self.log_records.len() + 1 > LOG_CAPACITY {
|
|
||||||
self.log_records.pop_back(); // Start cleaning events from back
|
|
||||||
}
|
|
||||||
// Eventually push front the new record
|
|
||||||
self.log_records.push_front(record);
|
|
||||||
// Update log
|
|
||||||
self.update_logbox();
|
|
||||||
// flag redraw
|
|
||||||
self.redraw = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add message to log events and also display it as an alert
|
|
||||||
pub(super) fn log_and_alert(&mut self, level: LogLevel, msg: String) {
|
|
||||||
self.mount_error(msg.as_str());
|
|
||||||
self.log(level, msg);
|
|
||||||
// Update log
|
|
||||||
self.update_logbox();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize configuration client if possible.
|
|
||||||
/// This function doesn't return errors.
|
|
||||||
pub(super) fn init_config_client() -> ConfigClient {
|
|
||||||
match environment::init_config_dir() {
|
|
||||||
Ok(termscp_dir) => match termscp_dir {
|
|
||||||
Some(termscp_dir) => {
|
|
||||||
// Make configuration file path and ssh keys path
|
|
||||||
let (config_path, ssh_keys_path): (PathBuf, PathBuf) =
|
|
||||||
environment::get_config_paths(termscp_dir.as_path());
|
|
||||||
match ConfigClient::new(config_path.as_path(), ssh_keys_path.as_path()) {
|
|
||||||
Ok(config_client) => config_client,
|
|
||||||
Err(_) => ConfigClient::degraded(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => ConfigClient::degraded(),
|
|
||||||
},
|
|
||||||
Err(_) => ConfigClient::degraded(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set text editor to use
|
|
||||||
pub(super) fn setup_text_editor(&self) {
|
|
||||||
unsafe {
|
|
||||||
env::set_var("EDITOR", self.config().get_text_editor());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a path to absolute according to host explorer
|
|
||||||
pub(super) fn host_bridge_to_abs_path(&self, path: &Path) -> PathBuf {
|
|
||||||
path::absolutize(self.host_bridge().wrkdir.as_path(), path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a path to absolute according to remote explorer
|
|
||||||
pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf {
|
|
||||||
path::absolutize(self.remote().wrkdir.as_path(), path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get remote hostname
|
|
||||||
pub(super) fn get_remote_hostname(&self) -> String {
|
|
||||||
let ft_params = self.context().remote_params().unwrap();
|
|
||||||
self.get_hostname(&ft_params.params)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn get_hostbridge_hostname(&self) -> String {
|
|
||||||
let host_bridge_params = self.context().host_bridge_params().unwrap();
|
|
||||||
match host_bridge_params {
|
|
||||||
HostBridgeParams::Localhost(_) => {
|
|
||||||
let hostname = match hostname::get() {
|
|
||||||
Ok(h) => h,
|
|
||||||
Err(_) => return String::from("localhost"),
|
|
||||||
};
|
|
||||||
let hostname: String = hostname.as_os_str().to_string_lossy().to_string();
|
|
||||||
let tokens: Vec<&str> = hostname.split('.').collect();
|
|
||||||
String::from(*tokens.first().unwrap_or(&"localhost"))
|
|
||||||
}
|
|
||||||
HostBridgeParams::Remote(_, params) => self.get_hostname(params),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_hostname(&self, params: &ProtocolParams) -> String {
|
|
||||||
match params {
|
|
||||||
ProtocolParams::Generic(params) => params.address.clone(),
|
|
||||||
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
|
|
||||||
ProtocolParams::Kube(params) => {
|
|
||||||
params.namespace.clone().unwrap_or("default".to_string())
|
|
||||||
}
|
|
||||||
ProtocolParams::Smb(params) => params.address.clone(),
|
|
||||||
ProtocolParams::WebDAV(params) => params.uri.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get connection message to show to client
|
|
||||||
pub(super) fn get_connection_msg(params: &ProtocolParams) -> String {
|
|
||||||
match params {
|
|
||||||
ProtocolParams::Generic(params) => {
|
|
||||||
info!(
|
|
||||||
"Client is not connected to remote; connecting to {}:{}",
|
|
||||||
params.address, params.port
|
|
||||||
);
|
|
||||||
format!("Connecting to {}:{}…", params.address, params.port)
|
|
||||||
}
|
|
||||||
ProtocolParams::AwsS3(params) => {
|
|
||||||
info!(
|
|
||||||
"Client is not connected to remote; connecting to {}{} ({})",
|
|
||||||
params.endpoint.as_deref().unwrap_or(""),
|
|
||||||
params.bucket_name,
|
|
||||||
params.region.as_deref().unwrap_or("custom")
|
|
||||||
);
|
|
||||||
format!("Connecting to {}…", params.bucket_name)
|
|
||||||
}
|
|
||||||
ProtocolParams::Kube(params) => {
|
|
||||||
let namespace = params.namespace.as_deref().unwrap_or("default");
|
|
||||||
info!("Client is not connected to remote; connecting to namespace {namespace}",);
|
|
||||||
format!("Connecting to Kube namespace {namespace}…",)
|
|
||||||
}
|
|
||||||
ProtocolParams::Smb(params) => {
|
|
||||||
info!(
|
|
||||||
"Client is not connected to remote; connecting to {}:{}",
|
|
||||||
params.address, params.share
|
|
||||||
);
|
|
||||||
format!("Connecting to \\\\{}\\{}…", params.address, params.share)
|
|
||||||
}
|
|
||||||
ProtocolParams::WebDAV(params) => {
|
|
||||||
info!(
|
|
||||||
"Client is not connected to remote; connecting to {}",
|
|
||||||
params.uri
|
|
||||||
);
|
|
||||||
format!("Connecting to {}…", params.uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send notification regarding transfer completed
|
|
||||||
/// The notification is sent only when these conditions are satisfied:
|
|
||||||
///
|
|
||||||
/// - notifications are enabled
|
|
||||||
/// - transfer size is greater or equal than notification threshold
|
|
||||||
pub(super) fn notify_transfer_completed(&self, payload: &TransferPayload) {
|
|
||||||
if self.config().get_notifications()
|
|
||||||
&& self.config().get_notification_threshold() as usize <= self.transfer.full_size()
|
|
||||||
{
|
|
||||||
Notification::transfer_completed(self.transfer_completed_msg(payload));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send notification regarding transfer error
|
|
||||||
/// The notification is sent only when these conditions are satisfied:
|
|
||||||
///
|
|
||||||
/// - notifications are enabled
|
|
||||||
/// - transfer size is greater or equal than notification threshold
|
|
||||||
pub(super) fn notify_transfer_error(&self, msg: &str) {
|
|
||||||
if self.config().get_notifications()
|
|
||||||
&& self.config().get_notification_threshold() as usize <= self.transfer.full_size()
|
|
||||||
{
|
|
||||||
Notification::transfer_error(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transfer_completed_msg(&self, payload: &TransferPayload) -> String {
|
|
||||||
let transfer_stats = format!(
|
|
||||||
"took {} seconds; at {}/s",
|
|
||||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
|
||||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
|
||||||
);
|
|
||||||
match payload {
|
|
||||||
TransferPayload::File(file) => {
|
|
||||||
format!(
|
|
||||||
"File \"{}\" has been successfully transferred ({})",
|
|
||||||
file.name(),
|
|
||||||
transfer_stats
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TransferPayload::Any(entry) => {
|
|
||||||
format!(
|
|
||||||
"\"{}\" has been successfully transferred ({})",
|
|
||||||
entry.name(),
|
|
||||||
transfer_stats
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TransferPayload::TransferQueue(entries) => {
|
|
||||||
format!(
|
|
||||||
"{} files has been successfully transferred ({})",
|
|
||||||
entries.len(),
|
|
||||||
transfer_stats
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update host bridge file list
|
|
||||||
pub(super) fn update_host_bridge_filelist(&mut self) {
|
|
||||||
self.reload_host_bridge_dir();
|
|
||||||
self.reload_host_bridge_filelist();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update host bridge file list
|
|
||||||
pub(super) fn reload_host_bridge_filelist(&mut self) {
|
|
||||||
// Get width
|
|
||||||
let width = self
|
|
||||||
.context_mut()
|
|
||||||
.terminal()
|
|
||||||
.raw()
|
|
||||||
.size()
|
|
||||||
.map(|x| (x.width / 2) - 2)
|
|
||||||
.unwrap_or(0) as usize;
|
|
||||||
let hostname = self.get_hostbridge_hostname();
|
|
||||||
|
|
||||||
let hostname: String = format!(
|
|
||||||
"{hostname}:{} ",
|
|
||||||
fmt_path_elide_ex(
|
|
||||||
self.host_bridge().wrkdir.as_path(),
|
|
||||||
width,
|
|
||||||
hostname.len() + 3
|
|
||||||
) // 3 because of '/…/'
|
|
||||||
);
|
|
||||||
let files: Vec<Vec<TextSpan>> = self
|
|
||||||
.host_bridge()
|
|
||||||
.iter_files()
|
|
||||||
.map(|x| {
|
|
||||||
let mut span = TextSpan::from(self.host_bridge().fmt_file(x));
|
|
||||||
if self.host_bridge().enqueued().contains_key(x.path()) {
|
|
||||||
span.modifiers |=
|
|
||||||
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC;
|
|
||||||
}
|
|
||||||
|
|
||||||
vec![span]
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
// Update content and title
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::ExplorerHostBridge,
|
|
||||||
Attribute::Content,
|
|
||||||
AttrValue::Table(files)
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::ExplorerHostBridge,
|
|
||||||
Attribute::Title,
|
|
||||||
AttrValue::Title((hostname, Alignment::Left))
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update remote file list
|
|
||||||
pub(super) fn update_remote_filelist(&mut self) {
|
|
||||||
self.reload_remote_dir();
|
|
||||||
self.reload_remote_filelist();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn get_tab_hostname(&self) -> String {
|
|
||||||
match self.browser.tab() {
|
|
||||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
|
||||||
self.get_hostbridge_hostname()
|
|
||||||
}
|
|
||||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.get_remote_hostname(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn terminal_prompt(&self) -> String {
|
|
||||||
const TERM_CYAN: &str = "\x1b[36m";
|
|
||||||
const TERM_GREEN: &str = "\x1b[32m";
|
|
||||||
const TERM_YELLOW: &str = "\x1b[33m";
|
|
||||||
const TERM_RESET: &str = "\x1b[0m";
|
|
||||||
|
|
||||||
let panel = self.browser.tab();
|
|
||||||
match panel {
|
|
||||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
|
||||||
let username = self
|
|
||||||
.context()
|
|
||||||
.host_bridge_params()
|
|
||||||
.and_then(|params| {
|
|
||||||
params
|
|
||||||
.username()
|
|
||||||
.map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@"))
|
|
||||||
})
|
|
||||||
.unwrap_or("".to_string());
|
|
||||||
let hostname = self.get_hostbridge_hostname();
|
|
||||||
format!(
|
|
||||||
"{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{}{TERM_RESET}$ ",
|
|
||||||
fmt_path_elide_ex(
|
|
||||||
self.host_bridge().wrkdir.as_path(),
|
|
||||||
0,
|
|
||||||
hostname.len() + 3 // 3 because of '/…/'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
|
|
||||||
let username = self
|
|
||||||
.context()
|
|
||||||
.remote_params()
|
|
||||||
.and_then(|params| {
|
|
||||||
params
|
|
||||||
.username()
|
|
||||||
.map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@"))
|
|
||||||
})
|
|
||||||
.unwrap_or("".to_string());
|
|
||||||
let hostname = self.get_remote_hostname();
|
|
||||||
let fmt_path = fmt_path_elide_ex(
|
|
||||||
self.remote().wrkdir.as_path(),
|
|
||||||
0,
|
|
||||||
hostname.len() + 3, // 3 because of '/…/'
|
|
||||||
);
|
|
||||||
let fmt_path = if fmt_path.starts_with('/') {
|
|
||||||
fmt_path
|
|
||||||
} else {
|
|
||||||
format!("/{}", fmt_path)
|
|
||||||
};
|
|
||||||
|
|
||||||
format!("{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{fmt_path}{TERM_RESET}$ ",)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn reload_remote_filelist(&mut self) {
|
|
||||||
let width = self
|
|
||||||
.context_mut()
|
|
||||||
.terminal()
|
|
||||||
.raw()
|
|
||||||
.size()
|
|
||||||
.map(|x| (x.width / 2) - 2)
|
|
||||||
.unwrap_or(0) as usize;
|
|
||||||
let hostname = self.get_remote_hostname();
|
|
||||||
let hostname: String = format!(
|
|
||||||
"{}:{} ",
|
|
||||||
hostname,
|
|
||||||
fmt_path_elide_ex(
|
|
||||||
self.remote().wrkdir.as_path(),
|
|
||||||
width,
|
|
||||||
hostname.len() + 3 // 3 because of '/…/'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
let files: Vec<Vec<TextSpan>> = self
|
|
||||||
.remote()
|
|
||||||
.iter_files()
|
|
||||||
.map(|x| {
|
|
||||||
let mut span = TextSpan::from(self.remote().fmt_file(x));
|
|
||||||
if self.remote().enqueued().contains_key(x.path()) {
|
|
||||||
span.modifiers |=
|
|
||||||
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC;
|
|
||||||
}
|
|
||||||
|
|
||||||
vec![span]
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
// Update content and title
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::ExplorerRemote,
|
|
||||||
Attribute::Content,
|
|
||||||
AttrValue::Table(files)
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::ExplorerRemote,
|
|
||||||
Attribute::Title,
|
|
||||||
AttrValue::Title((hostname, Alignment::Left))
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update log box
|
|
||||||
pub(super) fn update_logbox(&mut self) {
|
|
||||||
let mut table: TableBuilder = TableBuilder::default();
|
|
||||||
for (idx, record) in self.log_records.iter().enumerate() {
|
|
||||||
// Add row if not first row
|
|
||||||
if idx > 0 {
|
|
||||||
table.add_row();
|
|
||||||
}
|
|
||||||
let fg = match record.level {
|
|
||||||
LogLevel::Error => Color::Red,
|
|
||||||
LogLevel::Warn => Color::Yellow,
|
|
||||||
LogLevel::Info => Color::Green,
|
|
||||||
};
|
|
||||||
table
|
|
||||||
.add_col(TextSpan::from(format!(
|
|
||||||
"{}",
|
|
||||||
record.time.format("%Y-%m-%dT%H:%M:%S%Z")
|
|
||||||
)))
|
|
||||||
.add_col(TextSpan::from(" ["))
|
|
||||||
.add_col(
|
|
||||||
TextSpan::new(
|
|
||||||
format!(
|
|
||||||
"{:5}",
|
|
||||||
match record.level {
|
|
||||||
LogLevel::Error => "ERROR",
|
|
||||||
LogLevel::Warn => "WARN",
|
|
||||||
LogLevel::Info => "INFO",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)
|
|
||||||
.fg(fg),
|
|
||||||
)
|
|
||||||
.add_col(TextSpan::from("]: "))
|
|
||||||
.add_col(TextSpan::from(record.msg.as_str()));
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::Log,
|
|
||||||
Attribute::Content,
|
|
||||||
AttrValue::Table(table.build())
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn update_progress_bar(&mut self, filename: String) {
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::ProgressBarFull,
|
|
||||||
Attribute::Text,
|
|
||||||
AttrValue::String(self.transfer.full.to_string())
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::ProgressBarFull,
|
|
||||||
Attribute::Value,
|
|
||||||
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
|
||||||
self.transfer.full.calc_progress()
|
|
||||||
)))
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::ProgressBarPartial,
|
|
||||||
Attribute::Text,
|
|
||||||
AttrValue::String(self.transfer.partial.to_string())
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::ProgressBarPartial,
|
|
||||||
Attribute::Value,
|
|
||||||
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
|
||||||
self.transfer.partial.calc_progress()
|
|
||||||
)))
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::ProgressBarPartial,
|
|
||||||
Attribute::Title,
|
|
||||||
AttrValue::Title((filename, Alignment::Center))
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finalize find process
|
|
||||||
pub(super) fn finalize_find(&mut self) {
|
|
||||||
// Set found to none
|
|
||||||
self.browser.del_found();
|
|
||||||
// Restore tab
|
|
||||||
let new_tab = match self.browser.tab() {
|
|
||||||
FileExplorerTab::FindHostBridge => FileExplorerTab::HostBridge,
|
|
||||||
FileExplorerTab::FindRemote => FileExplorerTab::Remote,
|
|
||||||
_ => FileExplorerTab::HostBridge,
|
|
||||||
};
|
|
||||||
// Give focus to new tab
|
|
||||||
match new_tab {
|
|
||||||
FileExplorerTab::HostBridge => {
|
|
||||||
assert!(self.app.active(&Id::ExplorerHostBridge).is_ok())
|
|
||||||
}
|
|
||||||
FileExplorerTab::Remote => {
|
|
||||||
assert!(self.app.active(&Id::ExplorerRemote).is_ok())
|
|
||||||
}
|
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
|
||||||
assert!(self.app.active(&Id::ExplorerFind).is_ok())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.browser.change_tab(new_tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn update_find_list(&mut self) {
|
|
||||||
let files: Vec<Vec<TextSpan>> = self
|
|
||||||
.found()
|
|
||||||
.unwrap()
|
|
||||||
.iter_files()
|
|
||||||
.map(|x| {
|
|
||||||
let mut span = TextSpan::from(self.found().unwrap().fmt_file(x));
|
|
||||||
if self.found().unwrap().enqueued().contains_key(x.path()) {
|
|
||||||
span.modifiers |=
|
|
||||||
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC;
|
|
||||||
}
|
|
||||||
vec![span]
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.attr(
|
|
||||||
&Id::ExplorerFind,
|
|
||||||
Attribute::Content,
|
|
||||||
AttrValue::Table(files)
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn update_browser_file_list(&mut self) {
|
|
||||||
match self.browser.tab() {
|
|
||||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
|
||||||
self.update_host_bridge_filelist()
|
|
||||||
}
|
|
||||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn reload_browser_file_list(&mut self) {
|
|
||||||
match self.browser.tab() {
|
|
||||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
|
||||||
self.reload_host_bridge_filelist()
|
|
||||||
}
|
|
||||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.reload_remote_filelist(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn update_browser_file_list_swapped(&mut self) {
|
|
||||||
match self.browser.tab() {
|
|
||||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
|
||||||
self.update_remote_filelist()
|
|
||||||
}
|
|
||||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
|
|
||||||
self.update_host_bridge_filelist()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
use tuirealm::props::{
|
||||||
|
Alignment, AttrValue, Attribute, PropPayload, PropValue, TextModifiers, TextSpan,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::browser::FileExplorerTab;
|
||||||
|
use super::super::{FileTransferActivity, Id, ui_result};
|
||||||
|
use crate::utils::fmt::fmt_path_elide_ex;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
/// Update host bridge file list
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn update_host_bridge_filelist(&mut self) {
|
||||||
|
self.reload_host_bridge_dir();
|
||||||
|
self.reload_host_bridge_filelist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update host bridge file list
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn reload_host_bridge_filelist(&mut self) {
|
||||||
|
// Get width
|
||||||
|
let width = self
|
||||||
|
.context_mut()
|
||||||
|
.terminal()
|
||||||
|
.raw()
|
||||||
|
.size()
|
||||||
|
.map(|x| (x.width / 2) - 2)
|
||||||
|
.unwrap_or(0) as usize;
|
||||||
|
let hostname = self.get_hostbridge_hostname();
|
||||||
|
|
||||||
|
let hostname: String = format!(
|
||||||
|
"{hostname}:{} ",
|
||||||
|
fmt_path_elide_ex(
|
||||||
|
self.host_bridge().wrkdir.as_path(),
|
||||||
|
width,
|
||||||
|
hostname.len() + 3
|
||||||
|
) // 3 because of '/…/'
|
||||||
|
);
|
||||||
|
let files: Vec<Vec<TextSpan>> = self
|
||||||
|
.host_bridge()
|
||||||
|
.iter_files()
|
||||||
|
.map(|x| {
|
||||||
|
let mut span = TextSpan::from(self.host_bridge().fmt_file(x));
|
||||||
|
if self.host_bridge().enqueued().contains_key(x.path()) {
|
||||||
|
span.modifiers |=
|
||||||
|
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![span]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
// Update content and title
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::ExplorerHostBridge,
|
||||||
|
Attribute::Content,
|
||||||
|
AttrValue::Table(files),
|
||||||
|
));
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::ExplorerHostBridge,
|
||||||
|
Attribute::Title,
|
||||||
|
AttrValue::Title((hostname, Alignment::Left)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update remote file list
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn update_remote_filelist(&mut self) {
|
||||||
|
self.reload_remote_dir();
|
||||||
|
self.reload_remote_filelist();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn get_tab_hostname(&self) -> String {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||||
|
self.get_hostbridge_hostname()
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.get_remote_hostname(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn terminal_prompt(&self) -> String {
|
||||||
|
const TERM_CYAN: &str = "\x1b[36m";
|
||||||
|
const TERM_GREEN: &str = "\x1b[32m";
|
||||||
|
const TERM_YELLOW: &str = "\x1b[33m";
|
||||||
|
const TERM_RESET: &str = "\x1b[0m";
|
||||||
|
|
||||||
|
let panel = self.browser.tab();
|
||||||
|
match panel {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||||
|
let username = self
|
||||||
|
.context()
|
||||||
|
.host_bridge_params()
|
||||||
|
.and_then(|params| {
|
||||||
|
params
|
||||||
|
.username()
|
||||||
|
.map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@"))
|
||||||
|
})
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let hostname = self.get_hostbridge_hostname();
|
||||||
|
format!(
|
||||||
|
"{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{}{TERM_RESET}$ ",
|
||||||
|
fmt_path_elide_ex(
|
||||||
|
self.host_bridge().wrkdir.as_path(),
|
||||||
|
0,
|
||||||
|
hostname.len() + 3 // 3 because of '/…/'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
|
||||||
|
let username = self
|
||||||
|
.context()
|
||||||
|
.remote_params()
|
||||||
|
.and_then(|params| {
|
||||||
|
params
|
||||||
|
.username()
|
||||||
|
.map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@"))
|
||||||
|
})
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let hostname = self.get_remote_hostname();
|
||||||
|
let fmt_path = fmt_path_elide_ex(
|
||||||
|
self.remote().wrkdir.as_path(),
|
||||||
|
0,
|
||||||
|
hostname.len() + 3, // 3 because of '/…/'
|
||||||
|
);
|
||||||
|
let fmt_path = if fmt_path.starts_with('/') {
|
||||||
|
fmt_path
|
||||||
|
} else {
|
||||||
|
format!("/{}", fmt_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{fmt_path}{TERM_RESET}$ ",)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn reload_remote_filelist(&mut self) {
|
||||||
|
let width = self
|
||||||
|
.context_mut()
|
||||||
|
.terminal()
|
||||||
|
.raw()
|
||||||
|
.size()
|
||||||
|
.map(|x| (x.width / 2) - 2)
|
||||||
|
.unwrap_or(0) as usize;
|
||||||
|
let hostname = self.get_remote_hostname();
|
||||||
|
let hostname: String = format!(
|
||||||
|
"{}:{} ",
|
||||||
|
hostname,
|
||||||
|
fmt_path_elide_ex(
|
||||||
|
self.remote().wrkdir.as_path(),
|
||||||
|
width,
|
||||||
|
hostname.len() + 3 // 3 because of '/…/'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let files: Vec<Vec<TextSpan>> = self
|
||||||
|
.remote()
|
||||||
|
.iter_files()
|
||||||
|
.map(|x| {
|
||||||
|
let mut span = TextSpan::from(self.remote().fmt_file(x));
|
||||||
|
if self.remote().enqueued().contains_key(x.path()) {
|
||||||
|
span.modifiers |=
|
||||||
|
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![span]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
// Update content and title
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::ExplorerRemote,
|
||||||
|
Attribute::Content,
|
||||||
|
AttrValue::Table(files),
|
||||||
|
));
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::ExplorerRemote,
|
||||||
|
Attribute::Title,
|
||||||
|
AttrValue::Title((hostname, Alignment::Left)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn update_progress_bar(
|
||||||
|
&mut self,
|
||||||
|
filename: String,
|
||||||
|
) {
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::ProgressBarFull,
|
||||||
|
Attribute::Text,
|
||||||
|
AttrValue::String(self.transfer.full.to_string()),
|
||||||
|
));
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::ProgressBarFull,
|
||||||
|
Attribute::Value,
|
||||||
|
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
||||||
|
self.transfer.full.calc_progress(),
|
||||||
|
))),
|
||||||
|
));
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::ProgressBarPartial,
|
||||||
|
Attribute::Text,
|
||||||
|
AttrValue::String(self.transfer.partial.to_string()),
|
||||||
|
));
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::ProgressBarPartial,
|
||||||
|
Attribute::Value,
|
||||||
|
AttrValue::Payload(PropPayload::One(PropValue::F64(
|
||||||
|
self.transfer.partial.calc_progress(),
|
||||||
|
))),
|
||||||
|
));
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::ProgressBarPartial,
|
||||||
|
Attribute::Title,
|
||||||
|
AttrValue::Title((filename, Alignment::Center)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finalize find process
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn finalize_find(&mut self) {
|
||||||
|
// Set found to none
|
||||||
|
self.browser.del_found();
|
||||||
|
// Restore tab
|
||||||
|
let new_tab = match self.browser.tab() {
|
||||||
|
FileExplorerTab::FindHostBridge => FileExplorerTab::HostBridge,
|
||||||
|
FileExplorerTab::FindRemote => FileExplorerTab::Remote,
|
||||||
|
_ => FileExplorerTab::HostBridge,
|
||||||
|
};
|
||||||
|
// Give focus to new tab
|
||||||
|
match new_tab {
|
||||||
|
FileExplorerTab::HostBridge => {
|
||||||
|
ui_result(self.app.active(&Id::ExplorerHostBridge));
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote => {
|
||||||
|
ui_result(self.app.active(&Id::ExplorerRemote));
|
||||||
|
}
|
||||||
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||||
|
ui_result(self.app.active(&Id::ExplorerFind));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.browser.change_tab(new_tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn update_find_list(&mut self) {
|
||||||
|
let files: Vec<Vec<TextSpan>> = self
|
||||||
|
.found()
|
||||||
|
.unwrap()
|
||||||
|
.iter_files()
|
||||||
|
.map(|x| {
|
||||||
|
let mut span = TextSpan::from(self.found().unwrap().fmt_file(x));
|
||||||
|
if self.found().unwrap().enqueued().contains_key(x.path()) {
|
||||||
|
span.modifiers |=
|
||||||
|
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC;
|
||||||
|
}
|
||||||
|
vec![span]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::ExplorerFind,
|
||||||
|
Attribute::Content,
|
||||||
|
AttrValue::Table(files),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn update_browser_file_list(&mut self) {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||||
|
self.update_host_bridge_filelist()
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn reload_browser_file_list(&mut self) {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||||
|
self.reload_host_bridge_filelist()
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.reload_remote_filelist(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn update_browser_file_list_swapped(&mut self) {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||||
|
self.update_remote_filelist()
|
||||||
|
}
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
|
||||||
|
self.update_host_bridge_filelist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use super::super::{ConfigClient, FileTransferActivity};
|
||||||
|
use crate::filetransfer::{HostBridgeParams, ProtocolParams};
|
||||||
|
use crate::system::environment;
|
||||||
|
use crate::utils::path;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
/// Initialize configuration client if possible.
|
||||||
|
/// This function doesn't return errors.
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn init_config_client() -> ConfigClient {
|
||||||
|
match environment::init_config_dir() {
|
||||||
|
Ok(termscp_dir) => match termscp_dir {
|
||||||
|
Some(termscp_dir) => {
|
||||||
|
// Make configuration file path and ssh keys path
|
||||||
|
let (config_path, ssh_keys_path): (PathBuf, PathBuf) =
|
||||||
|
environment::get_config_paths(termscp_dir.as_path());
|
||||||
|
match ConfigClient::new(config_path.as_path(), ssh_keys_path.as_path()) {
|
||||||
|
Ok(config_client) => config_client,
|
||||||
|
Err(_) => ConfigClient::degraded(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => ConfigClient::degraded(),
|
||||||
|
},
|
||||||
|
Err(_) => ConfigClient::degraded(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set text editor to use
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn setup_text_editor(&self) {
|
||||||
|
unsafe {
|
||||||
|
env::set_var("EDITOR", self.config().get_text_editor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a path to absolute according to the current tab's pane explorer
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn pane_to_abs_path(&self, path: &Path) -> PathBuf {
|
||||||
|
path::absolutize(self.browser.fs_pane().explorer.wrkdir.as_path(), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get remote hostname
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn get_remote_hostname(&self) -> String {
|
||||||
|
let ft_params = self.context().remote_params().unwrap();
|
||||||
|
self.get_hostname(&ft_params.params)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn get_hostbridge_hostname(&self) -> String {
|
||||||
|
let host_bridge_params = self.context().host_bridge_params().unwrap();
|
||||||
|
match host_bridge_params {
|
||||||
|
HostBridgeParams::Localhost(_) => {
|
||||||
|
let hostname = match hostname::get() {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(_) => return String::from("localhost"),
|
||||||
|
};
|
||||||
|
let hostname: String = hostname.as_os_str().to_string_lossy().to_string();
|
||||||
|
let tokens: Vec<&str> = hostname.split('.').collect();
|
||||||
|
String::from(*tokens.first().unwrap_or(&"localhost"))
|
||||||
|
}
|
||||||
|
HostBridgeParams::Remote(_, params) => self.get_hostname(params),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_hostname(&self, params: &ProtocolParams) -> String {
|
||||||
|
match params {
|
||||||
|
ProtocolParams::Generic(params) => params.address.clone(),
|
||||||
|
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
|
||||||
|
ProtocolParams::Kube(params) => {
|
||||||
|
params.namespace.clone().unwrap_or("default".to_string())
|
||||||
|
}
|
||||||
|
ProtocolParams::Smb(params) => params.address.clone(),
|
||||||
|
ProtocolParams::WebDAV(params) => params.uri.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get connection message to show to client
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn get_connection_msg(
|
||||||
|
params: &ProtocolParams,
|
||||||
|
) -> String {
|
||||||
|
match params {
|
||||||
|
ProtocolParams::Generic(params) => {
|
||||||
|
info!(
|
||||||
|
"Client is not connected to remote; connecting to {}:{}",
|
||||||
|
params.address, params.port
|
||||||
|
);
|
||||||
|
format!("Connecting to {}:{}…", params.address, params.port)
|
||||||
|
}
|
||||||
|
ProtocolParams::AwsS3(params) => {
|
||||||
|
info!(
|
||||||
|
"Client is not connected to remote; connecting to {}{} ({})",
|
||||||
|
params.endpoint.as_deref().unwrap_or(""),
|
||||||
|
params.bucket_name,
|
||||||
|
params.region.as_deref().unwrap_or("custom")
|
||||||
|
);
|
||||||
|
format!("Connecting to {}…", params.bucket_name)
|
||||||
|
}
|
||||||
|
ProtocolParams::Kube(params) => {
|
||||||
|
let namespace = params.namespace.as_deref().unwrap_or("default");
|
||||||
|
info!("Client is not connected to remote; connecting to namespace {namespace}",);
|
||||||
|
format!("Connecting to Kube namespace {namespace}…",)
|
||||||
|
}
|
||||||
|
ProtocolParams::Smb(params) => {
|
||||||
|
info!(
|
||||||
|
"Client is not connected to remote; connecting to {}:{}",
|
||||||
|
params.address, params.share
|
||||||
|
);
|
||||||
|
format!("Connecting to \\\\{}\\{}…", params.address, params.share)
|
||||||
|
}
|
||||||
|
ProtocolParams::WebDAV(params) => {
|
||||||
|
info!(
|
||||||
|
"Client is not connected to remote; connecting to {}",
|
||||||
|
params.uri
|
||||||
|
);
|
||||||
|
format!("Connecting to {}…", params.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use tuirealm::props::{AttrValue, Attribute, Color, TableBuilder, TextSpan};
|
||||||
|
|
||||||
|
use super::super::{FileTransferActivity, Id, LogLevel, LogRecord, ui_result};
|
||||||
|
|
||||||
|
const LOG_CAPACITY: usize = 256;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
/// Add message to log events
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn log(&mut self, level: LogLevel, msg: String) {
|
||||||
|
// Log to file
|
||||||
|
match level {
|
||||||
|
LogLevel::Error => error!("{}", msg),
|
||||||
|
LogLevel::Info => info!("{}", msg),
|
||||||
|
LogLevel::Warn => warn!("{}", msg),
|
||||||
|
}
|
||||||
|
// Create log record
|
||||||
|
let record: LogRecord = LogRecord::new(level, msg);
|
||||||
|
//Check if history overflows the size
|
||||||
|
if self.log_records.len() + 1 > LOG_CAPACITY {
|
||||||
|
self.log_records.pop_back(); // Start cleaning events from back
|
||||||
|
}
|
||||||
|
// Eventually push front the new record
|
||||||
|
self.log_records.push_front(record);
|
||||||
|
// Update log
|
||||||
|
self.update_logbox();
|
||||||
|
// flag redraw
|
||||||
|
self.redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add message to log events and also display it as an alert
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn log_and_alert(
|
||||||
|
&mut self,
|
||||||
|
level: LogLevel,
|
||||||
|
msg: String,
|
||||||
|
) {
|
||||||
|
self.mount_error(msg.as_str());
|
||||||
|
self.log(level, msg);
|
||||||
|
// Update log
|
||||||
|
self.update_logbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update log box
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn update_logbox(&mut self) {
|
||||||
|
let mut table: TableBuilder = TableBuilder::default();
|
||||||
|
for (idx, record) in self.log_records.iter().enumerate() {
|
||||||
|
// Add row if not first row
|
||||||
|
if idx > 0 {
|
||||||
|
table.add_row();
|
||||||
|
}
|
||||||
|
let fg = match record.level {
|
||||||
|
LogLevel::Error => Color::Red,
|
||||||
|
LogLevel::Warn => Color::Yellow,
|
||||||
|
LogLevel::Info => Color::Green,
|
||||||
|
};
|
||||||
|
table
|
||||||
|
.add_col(TextSpan::from(format!(
|
||||||
|
"{}",
|
||||||
|
record.time.format("%Y-%m-%dT%H:%M:%S%Z")
|
||||||
|
)))
|
||||||
|
.add_col(TextSpan::from(" ["))
|
||||||
|
.add_col(
|
||||||
|
TextSpan::new(
|
||||||
|
format!(
|
||||||
|
"{:5}",
|
||||||
|
match record.level {
|
||||||
|
LogLevel::Error => "ERROR",
|
||||||
|
LogLevel::Warn => "WARN",
|
||||||
|
LogLevel::Info => "INFO",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.fg(fg),
|
||||||
|
)
|
||||||
|
.add_col(TextSpan::from("]: "))
|
||||||
|
.add_col(TextSpan::from(record.msg.as_str()));
|
||||||
|
}
|
||||||
|
ui_result(self.app.attr(
|
||||||
|
&Id::Log,
|
||||||
|
Attribute::Content,
|
||||||
|
AttrValue::Table(table.build()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use bytesize::ByteSize;
|
||||||
|
|
||||||
|
use super::super::{FileTransferActivity, TransferPayload};
|
||||||
|
use crate::system::notifications::Notification;
|
||||||
|
use crate::utils::fmt::fmt_millis;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
/// Send notification regarding transfer completed
|
||||||
|
/// The notification is sent only when these conditions are satisfied:
|
||||||
|
///
|
||||||
|
/// - notifications are enabled
|
||||||
|
/// - transfer size is greater or equal than notification threshold
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn notify_transfer_completed(
|
||||||
|
&self,
|
||||||
|
payload: &TransferPayload,
|
||||||
|
) {
|
||||||
|
if self.config().get_notifications()
|
||||||
|
&& self.config().get_notification_threshold() as usize <= self.transfer.full_size()
|
||||||
|
{
|
||||||
|
Notification::transfer_completed(self.transfer_completed_msg(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send notification regarding transfer error
|
||||||
|
/// The notification is sent only when these conditions are satisfied:
|
||||||
|
///
|
||||||
|
/// - notifications are enabled
|
||||||
|
/// - transfer size is greater or equal than notification threshold
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn notify_transfer_error(&self, msg: &str) {
|
||||||
|
if self.config().get_notifications()
|
||||||
|
&& self.config().get_notification_threshold() as usize <= self.transfer.full_size()
|
||||||
|
{
|
||||||
|
Notification::transfer_error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer_completed_msg(&self, payload: &TransferPayload) -> String {
|
||||||
|
let transfer_stats = format!(
|
||||||
|
"took {} seconds; at {}/s",
|
||||||
|
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||||
|
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||||
|
);
|
||||||
|
match payload {
|
||||||
|
TransferPayload::File(file) => {
|
||||||
|
format!(
|
||||||
|
"File \"{}\" has been successfully transferred ({})",
|
||||||
|
file.name(),
|
||||||
|
transfer_stats
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TransferPayload::Any(entry) => {
|
||||||
|
format!(
|
||||||
|
"\"{}\" has been successfully transferred ({})",
|
||||||
|
entry.name(),
|
||||||
|
transfer_stats
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TransferPayload::TransferQueue(entries) => {
|
||||||
|
format!(
|
||||||
|
"{} files has been successfully transferred ({})",
|
||||||
|
entries.len(),
|
||||||
|
transfer_stats
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// This module is split into files, cause it's just too big
|
// This module is split into files, cause it's just too big
|
||||||
mod actions;
|
mod actions;
|
||||||
@@ -20,10 +20,10 @@ use std::time::Duration;
|
|||||||
// Includes
|
// Includes
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use lib::browser;
|
use lib::browser;
|
||||||
use lib::browser::Browser;
|
use lib::browser::{Browser, FileExplorerTab};
|
||||||
|
use lib::pane::Pane;
|
||||||
use lib::transfer::{TransferOpts, TransferStates};
|
use lib::transfer::{TransferOpts, TransferStates};
|
||||||
use lib::walkdir::WalkdirStates;
|
use lib::walkdir::WalkdirStates;
|
||||||
use remotefs::RemoteFs;
|
|
||||||
use session::TransferPayload;
|
use session::TransferPayload;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
||||||
@@ -34,7 +34,7 @@ use crate::explorer::{FileExplorer, FileSorting};
|
|||||||
use crate::filetransfer::{
|
use crate::filetransfer::{
|
||||||
FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder,
|
FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder,
|
||||||
};
|
};
|
||||||
use crate::host::HostBridge;
|
use crate::host::RemoteBridged;
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
use crate::system::watcher::FsWatcher;
|
use crate::system::watcher::FsWatcher;
|
||||||
|
|
||||||
@@ -237,10 +237,6 @@ pub struct FileTransferActivity {
|
|||||||
app: Application<Id, Msg, NoUserEvent>,
|
app: Application<Id, Msg, NoUserEvent>,
|
||||||
/// Whether should redraw UI
|
/// Whether should redraw UI
|
||||||
redraw: bool,
|
redraw: bool,
|
||||||
/// Host bridge
|
|
||||||
host_bridge: Box<dyn HostBridge>,
|
|
||||||
/// Remote host client
|
|
||||||
client: Box<dyn RemoteFs>,
|
|
||||||
/// Browser
|
/// Browser
|
||||||
browser: Browser,
|
browser: Browser,
|
||||||
/// Current log lines
|
/// Current log lines
|
||||||
@@ -253,10 +249,6 @@ pub struct FileTransferActivity {
|
|||||||
cache: Option<TempDir>,
|
cache: Option<TempDir>,
|
||||||
/// Fs watcher
|
/// Fs watcher
|
||||||
fswatcher: Option<FsWatcher>,
|
fswatcher: Option<FsWatcher>,
|
||||||
/// host bridge connected
|
|
||||||
host_bridge_connected: bool,
|
|
||||||
/// remote connected once
|
|
||||||
remote_connected: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -272,6 +264,25 @@ impl FileTransferActivity {
|
|||||||
let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client)?;
|
let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client)?;
|
||||||
let host_bridge_connected = host_bridge.is_localhost();
|
let host_bridge_connected = host_bridge.is_localhost();
|
||||||
let enable_fs_watcher = host_bridge.is_localhost();
|
let enable_fs_watcher = host_bridge.is_localhost();
|
||||||
|
// Build remote client, wrapped as HostBridge via RemoteBridged
|
||||||
|
let remote_client = RemoteFsBuilder::build(
|
||||||
|
remote_params.protocol,
|
||||||
|
remote_params.params.clone(),
|
||||||
|
&config_client,
|
||||||
|
)?;
|
||||||
|
let remote_fs: Box<dyn crate::host::HostBridge> =
|
||||||
|
Box::new(RemoteBridged::from(remote_client));
|
||||||
|
// Build panes
|
||||||
|
let local_pane = Pane::new(
|
||||||
|
Browser::build_local_explorer(&config_client),
|
||||||
|
host_bridge_connected,
|
||||||
|
host_bridge,
|
||||||
|
);
|
||||||
|
let remote_pane = Pane::new(
|
||||||
|
Browser::build_remote_explorer(&config_client),
|
||||||
|
false,
|
||||||
|
remote_fs,
|
||||||
|
);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
exit_reason: None,
|
exit_reason: None,
|
||||||
context: None,
|
context: None,
|
||||||
@@ -281,14 +292,8 @@ impl FileTransferActivity {
|
|||||||
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
||||||
),
|
),
|
||||||
redraw: true,
|
redraw: true,
|
||||||
host_bridge,
|
browser: Browser::new(local_pane, remote_pane),
|
||||||
client: RemoteFsBuilder::build(
|
log_records: VecDeque::with_capacity(256),
|
||||||
remote_params.protocol,
|
|
||||||
remote_params.params.clone(),
|
|
||||||
&config_client,
|
|
||||||
)?,
|
|
||||||
browser: Browser::new(&config_client),
|
|
||||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
|
||||||
walkdir: WalkdirStates::default(),
|
walkdir: WalkdirStates::default(),
|
||||||
transfer: TransferStates::default(),
|
transfer: TransferStates::default(),
|
||||||
cache: TempDir::new().ok(),
|
cache: TempDir::new().ok(),
|
||||||
@@ -297,11 +302,18 @@ impl FileTransferActivity {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
host_bridge_connected,
|
|
||||||
remote_connected: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the active tab targets the local side
|
||||||
|
/// (either the main host-bridge pane or a find-result rooted there).
|
||||||
|
fn is_local_tab(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.browser.tab(),
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn host_bridge(&self) -> &FileExplorer {
|
fn host_bridge(&self) -> &FileExplorer {
|
||||||
self.browser.host_bridge()
|
self.browser.host_bridge()
|
||||||
}
|
}
|
||||||
@@ -433,7 +445,7 @@ impl Activity for FileTransferActivity {
|
|||||||
error!("Failed to enter raw mode: {}", err);
|
error!("Failed to enter raw mode: {}", err);
|
||||||
}
|
}
|
||||||
// Get files at current pwd
|
// Get files at current pwd
|
||||||
if self.host_bridge.is_localhost() {
|
if self.browser.local_pane().fs.is_localhost() {
|
||||||
debug!("Reloading host bridge directory");
|
debug!("Reloading host bridge directory");
|
||||||
self.reload_host_bridge_dir();
|
self.reload_host_bridge_dir();
|
||||||
}
|
}
|
||||||
@@ -460,9 +472,10 @@ impl Activity for FileTransferActivity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check if connected to host bridge (popup must be None, otherwise would try reconnecting in loop in case of error)
|
// Check if connected to host bridge (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||||
if (!self.host_bridge.is_connected() || !self.host_bridge_connected)
|
if (!self.browser.local_pane_mut().fs.is_connected()
|
||||||
|
|| !self.browser.local_pane().connected)
|
||||||
&& !self.app.mounted(&Id::FatalPopup)
|
&& !self.app.mounted(&Id::FatalPopup)
|
||||||
&& !self.host_bridge.is_localhost()
|
&& !self.browser.local_pane().fs.is_localhost()
|
||||||
{
|
{
|
||||||
let host_bridge_params = self.context().host_bridge_params().unwrap();
|
let host_bridge_params = self.context().host_bridge_params().unwrap();
|
||||||
let ft_params = host_bridge_params.unwrap_protocol_params();
|
let ft_params = host_bridge_params.unwrap_protocol_params();
|
||||||
@@ -476,9 +489,10 @@ impl Activity for FileTransferActivity {
|
|||||||
self.redraw = true;
|
self.redraw = true;
|
||||||
}
|
}
|
||||||
// Check if connected to remote (popup must be None, otherwise would try reconnecting in loop in case of error)
|
// Check if connected to remote (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||||
if (!self.client.is_connected() || !self.remote_connected)
|
if (!self.browser.remote_pane_mut().fs.is_connected()
|
||||||
|
|| !self.browser.remote_pane().connected)
|
||||||
&& !self.app.mounted(&Id::FatalPopup)
|
&& !self.app.mounted(&Id::FatalPopup)
|
||||||
&& self.host_bridge.is_connected()
|
&& self.browser.local_pane_mut().fs.is_connected()
|
||||||
{
|
{
|
||||||
let ftparams = self.context().remote_params().unwrap();
|
let ftparams = self.context().remote_params().unwrap();
|
||||||
// print params
|
// print params
|
||||||
@@ -522,14 +536,21 @@ impl Activity for FileTransferActivity {
|
|||||||
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
||||||
error!("Failed to clear screen: {}", err);
|
error!("Failed to clear screen: {}", err);
|
||||||
}
|
}
|
||||||
// Disconnect client
|
// Disconnect remote
|
||||||
if self.client.is_connected() {
|
if self.browser.remote_pane_mut().fs.is_connected() {
|
||||||
let _ = self.client.disconnect();
|
let _ = self.browser.remote_pane_mut().fs.disconnect();
|
||||||
}
|
}
|
||||||
// disconnect host bridge
|
// Disconnect host bridge
|
||||||
if self.host_bridge.is_connected() {
|
if self.browser.local_pane_mut().fs.is_connected() {
|
||||||
let _ = self.host_bridge.disconnect();
|
let _ = self.browser.local_pane_mut().fs.disconnect();
|
||||||
}
|
}
|
||||||
self.context.take()
|
self.context.take()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Log a UI operation error instead of panicking.
|
||||||
|
fn ui_result<T>(result: Result<T, impl std::fmt::Display>) {
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("UI operation failed: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn connect_to_host_bridge(&mut self) {
|
||||||
|
let ft_params = self.context().remote_params().unwrap().clone();
|
||||||
|
let entry_dir: Option<PathBuf> = ft_params.local_path;
|
||||||
|
// Connect to host bridge
|
||||||
|
match self.browser.local_pane_mut().fs.connect() {
|
||||||
|
Ok(()) => {
|
||||||
|
let connected = self.browser.local_pane_mut().fs.is_connected();
|
||||||
|
self.browser.local_pane_mut().connected = connected;
|
||||||
|
if !connected {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log welcome
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"Established connection with '{}'",
|
||||||
|
self.get_hostbridge_hostname()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to change directory to entry directory
|
||||||
|
if let Some(entry_path) = entry_dir {
|
||||||
|
self.local_changedir(entry_path.as_path(), false);
|
||||||
|
}
|
||||||
|
// Set state to explorer
|
||||||
|
self.umount_wait();
|
||||||
|
self.reload_host_bridge_dir();
|
||||||
|
// Update file lists
|
||||||
|
self.update_host_bridge_filelist();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Set popup fatal error
|
||||||
|
self.umount_wait();
|
||||||
|
self.mount_fatal(err.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to remote
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn connect_to_remote(&mut self) {
|
||||||
|
let ft_params = self.context().remote_params().unwrap().clone();
|
||||||
|
let entry_dir: Option<PathBuf> = ft_params.remote_path;
|
||||||
|
// Connect to remote (banner is lost when using HostBridge API)
|
||||||
|
match self.browser.remote_pane_mut().fs.connect() {
|
||||||
|
Ok(()) => {
|
||||||
|
let connected = self.browser.remote_pane_mut().fs.is_connected();
|
||||||
|
self.browser.remote_pane_mut().connected = connected;
|
||||||
|
if !connected {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log welcome
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"Established connection with '{}'",
|
||||||
|
self.get_remote_hostname()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Try to change directory to entry directory
|
||||||
|
if let Some(entry_path) = entry_dir {
|
||||||
|
self.remote_changedir(entry_path.as_path(), false);
|
||||||
|
}
|
||||||
|
// Set state to explorer
|
||||||
|
self.umount_wait();
|
||||||
|
self.reload_remote_dir();
|
||||||
|
// Update file lists
|
||||||
|
self.update_host_bridge_filelist();
|
||||||
|
self.update_remote_filelist();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Set popup fatal error
|
||||||
|
self.umount_wait();
|
||||||
|
self.mount_fatal(err.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// disconnect from remote
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn disconnect(&mut self) {
|
||||||
|
let msg: String = format!("Disconnecting from {}…", self.get_remote_hostname());
|
||||||
|
// Show popup disconnecting
|
||||||
|
self.mount_wait(msg.as_str());
|
||||||
|
// Disconnect
|
||||||
|
let _ = self.browser.remote_pane_mut().fs.disconnect();
|
||||||
|
// Quit
|
||||||
|
self.exit_reason = Some(super::super::ExitReason::Disconnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// disconnect from remote and then quit
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn disconnect_and_quit(&mut self) {
|
||||||
|
self.disconnect();
|
||||||
|
self.exit_reason = Some(super::super::ExitReason::Quit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use remotefs::fs::{File, Metadata};
|
||||||
|
|
||||||
|
use super::transfer::TransferPayload;
|
||||||
|
use crate::host::HostError;
|
||||||
|
use crate::ui::activities::filetransfer::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
// -- reload directory --
|
||||||
|
|
||||||
|
/// Reload remote directory entries and update browser
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn reload_remote_dir(&mut self) {
|
||||||
|
self.reload_dir_on(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload host_bridge directory entries and update browser
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn reload_host_bridge_dir(&mut self) {
|
||||||
|
self.reload_dir_on(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload directory entries for the specified side.
|
||||||
|
fn reload_dir_on(&mut self, local: bool) {
|
||||||
|
let pane = if local {
|
||||||
|
self.browser.local_pane()
|
||||||
|
} else {
|
||||||
|
self.browser.remote_pane()
|
||||||
|
};
|
||||||
|
if !pane.connected {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mount_blocking_wait("Loading directory...");
|
||||||
|
|
||||||
|
let pane = if local {
|
||||||
|
self.browser.local_pane_mut()
|
||||||
|
} else {
|
||||||
|
self.browser.remote_pane_mut()
|
||||||
|
};
|
||||||
|
let wrkdir = match pane.fs.pwd() {
|
||||||
|
Ok(wrkdir) => wrkdir,
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not scan current directory: {err}"),
|
||||||
|
);
|
||||||
|
self.umount_wait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = self.scan_on(local, wrkdir.as_path());
|
||||||
|
|
||||||
|
self.umount_wait();
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(_) => {
|
||||||
|
let explorer = if local {
|
||||||
|
self.host_bridge_mut()
|
||||||
|
} else {
|
||||||
|
self.remote_mut()
|
||||||
|
};
|
||||||
|
explorer.wrkdir = wrkdir;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not scan current directory: {err}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan a directory on the specified side and update the explorer file list.
|
||||||
|
fn scan_on(&mut self, local: bool, path: &Path) -> Result<(), HostError> {
|
||||||
|
let pane = if local {
|
||||||
|
self.browser.local_pane_mut()
|
||||||
|
} else {
|
||||||
|
self.browser.remote_pane_mut()
|
||||||
|
};
|
||||||
|
let files = pane.fs.list_dir(path)?;
|
||||||
|
|
||||||
|
let explorer = if local {
|
||||||
|
self.host_bridge_mut()
|
||||||
|
} else {
|
||||||
|
self.remote_mut()
|
||||||
|
};
|
||||||
|
explorer.set_files(files);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- change directory --
|
||||||
|
|
||||||
|
/// Change directory on the current tab's pane (no reload).
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn pane_changedir(
|
||||||
|
&mut self,
|
||||||
|
path: &Path,
|
||||||
|
push: bool,
|
||||||
|
) {
|
||||||
|
let prev_dir: PathBuf = self.browser.fs_pane().explorer.wrkdir.clone();
|
||||||
|
match self.browser.fs_pane_mut().fs.change_wrkdir(path) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Changed directory: {}", path.display()),
|
||||||
|
);
|
||||||
|
if push {
|
||||||
|
self.browser
|
||||||
|
.fs_pane_mut()
|
||||||
|
.explorer
|
||||||
|
.pushd(prev_dir.as_path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not change working directory: {err}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change directory on the local pane and reload.
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn local_changedir(
|
||||||
|
&mut self,
|
||||||
|
path: &Path,
|
||||||
|
push: bool,
|
||||||
|
) {
|
||||||
|
let prev_dir: PathBuf = self.host_bridge().wrkdir.clone();
|
||||||
|
match self.browser.local_pane_mut().fs.change_wrkdir(path) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Changed directory on host bridge: {}", path.display()),
|
||||||
|
);
|
||||||
|
self.reload_host_bridge_dir();
|
||||||
|
if push {
|
||||||
|
self.host_bridge_mut().pushd(prev_dir.as_path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not change working directory: {err}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change directory on the remote pane and reload.
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn remote_changedir(
|
||||||
|
&mut self,
|
||||||
|
path: &Path,
|
||||||
|
push: bool,
|
||||||
|
) {
|
||||||
|
let prev_dir: PathBuf = self.remote().wrkdir.clone();
|
||||||
|
match self.browser.remote_pane_mut().fs.change_wrkdir(path) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Changed directory on remote: {}", path.display()),
|
||||||
|
);
|
||||||
|
self.reload_remote_dir();
|
||||||
|
if push {
|
||||||
|
self.remote_mut().pushd(prev_dir.as_path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not change working directory: {err}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- temporary file download --
|
||||||
|
|
||||||
|
/// Download provided file as a temporary file
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn download_file_as_temp(
|
||||||
|
&mut self,
|
||||||
|
file: &File,
|
||||||
|
) -> Result<PathBuf, String> {
|
||||||
|
let tmpfile: PathBuf = match self.cache.as_ref() {
|
||||||
|
Some(cache) => {
|
||||||
|
let mut p: PathBuf = cache.path().to_path_buf();
|
||||||
|
p.push(file.name());
|
||||||
|
p
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(String::from(
|
||||||
|
"Could not create tempfile: cache not available",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match self.filetransfer_recv(
|
||||||
|
TransferPayload::File(file.clone()),
|
||||||
|
tmpfile.as_path(),
|
||||||
|
Some(file.name()),
|
||||||
|
) {
|
||||||
|
Err(err) => Err(format!(
|
||||||
|
"Could not download {} to temporary file: {}",
|
||||||
|
file.path.display(),
|
||||||
|
err
|
||||||
|
)),
|
||||||
|
Ok(()) => Ok(tmpfile),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- transfer sizes --
|
||||||
|
|
||||||
|
/// Get total size of transfer for the specified side.
|
||||||
|
pub(super) fn get_total_transfer_size(&mut self, entry: &File, local: bool) -> usize {
|
||||||
|
self.mount_blocking_wait("Calculating transfer size…");
|
||||||
|
|
||||||
|
let sz = if entry.is_dir() {
|
||||||
|
let list_result = if local {
|
||||||
|
self.browser.local_pane_mut().fs.list_dir(entry.path())
|
||||||
|
} else {
|
||||||
|
self.browser.remote_pane_mut().fs.list_dir(entry.path())
|
||||||
|
};
|
||||||
|
match list_result {
|
||||||
|
Ok(files) => files
|
||||||
|
.iter()
|
||||||
|
.map(|x| self.get_total_transfer_size(x, local))
|
||||||
|
.sum(),
|
||||||
|
Err(err) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not list directory {}: {}",
|
||||||
|
entry.path().display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry.metadata.size as usize
|
||||||
|
};
|
||||||
|
|
||||||
|
self.umount_wait();
|
||||||
|
sz
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- file changed --
|
||||||
|
|
||||||
|
/// Check whether a file has changed on the specified side, compared to the given metadata.
|
||||||
|
pub(super) fn has_file_changed(
|
||||||
|
&mut self,
|
||||||
|
path: &Path,
|
||||||
|
other_metadata: &Metadata,
|
||||||
|
local: bool,
|
||||||
|
) -> bool {
|
||||||
|
let stat_result = if local {
|
||||||
|
self.browser.local_pane_mut().fs.stat(path)
|
||||||
|
} else {
|
||||||
|
self.browser.remote_pane_mut().fs.stat(path)
|
||||||
|
};
|
||||||
|
if let Ok(file) = stat_result {
|
||||||
|
other_metadata.modified != file.metadata().modified
|
||||||
|
|| other_metadata.size != file.metadata().size
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- file exist --
|
||||||
|
|
||||||
|
/// Check whether a file exists on the specified side.
|
||||||
|
pub(crate) fn file_exists(&mut self, p: &Path, local: bool) -> bool {
|
||||||
|
let pane = if local {
|
||||||
|
self.browser.local_pane_mut()
|
||||||
|
} else {
|
||||||
|
self.browser.remote_pane_mut()
|
||||||
|
};
|
||||||
|
pane.fs.exists(p).unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,841 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use bytesize::ByteSize;
|
||||||
|
use remotefs::fs::File;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::host::HostError;
|
||||||
|
use crate::ui::activities::filetransfer::{FileTransferActivity, LogLevel};
|
||||||
|
use crate::utils::fmt::fmt_millis;
|
||||||
|
|
||||||
|
/// Buffer size for remote I/O
|
||||||
|
const BUFSIZE: usize = 65535;
|
||||||
|
|
||||||
|
/// Describes the reason that caused an error during a file transfer
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
enum TransferErrorReason {
|
||||||
|
#[error("File transfer aborted")]
|
||||||
|
Abrupted,
|
||||||
|
#[error("I/O error on host_bridge: {0}")]
|
||||||
|
HostIoError(std::io::Error),
|
||||||
|
#[error("Host error: {0}")]
|
||||||
|
HostError(HostError),
|
||||||
|
#[error("I/O error on remote: {0}")]
|
||||||
|
RemoteIoError(std::io::Error),
|
||||||
|
#[error("Remote error: {0}")]
|
||||||
|
RemoteHostError(HostError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the entity to send or receive during a transfer.
|
||||||
|
/// - File: describes an individual `File` to send
|
||||||
|
/// - Any: Can be any kind of `File`, but just one
|
||||||
|
/// - Many: a list of `File`
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(in crate::ui::activities::filetransfer) enum TransferPayload {
|
||||||
|
File(File),
|
||||||
|
Any(File),
|
||||||
|
/// List of file with their destination name
|
||||||
|
TransferQueue(Vec<(File, PathBuf)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
/// Send fs entry to remote.
|
||||||
|
/// If dst_name is Some, entry will be saved with a different name.
|
||||||
|
/// If entry is a directory, this applies to directory only
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn filetransfer_send(
|
||||||
|
&mut self,
|
||||||
|
payload: TransferPayload,
|
||||||
|
curr_remote_path: &Path,
|
||||||
|
dst_name: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Use different method based on payload
|
||||||
|
let result = match payload {
|
||||||
|
TransferPayload::Any(ref entry) => {
|
||||||
|
self.filetransfer_send_any(entry, curr_remote_path, dst_name)
|
||||||
|
}
|
||||||
|
TransferPayload::File(ref file) => {
|
||||||
|
self.filetransfer_send_file(file, curr_remote_path, dst_name)
|
||||||
|
}
|
||||||
|
TransferPayload::TransferQueue(ref entries) => {
|
||||||
|
self.filetransfer_send_transfer_queue(entries)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Notify
|
||||||
|
match &result {
|
||||||
|
Ok(_) => {
|
||||||
|
self.notify_transfer_completed(&payload);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.notify_transfer_error(e.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send one file to remote at specified path.
|
||||||
|
fn filetransfer_send_file(
|
||||||
|
&mut self,
|
||||||
|
file: &File,
|
||||||
|
curr_remote_path: &Path,
|
||||||
|
dst_name: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Reset states
|
||||||
|
self.transfer.reset();
|
||||||
|
// Calculate total size of transfer
|
||||||
|
let total_transfer_size: usize = file.metadata.size as usize;
|
||||||
|
self.transfer.full.init(total_transfer_size);
|
||||||
|
// Mount progress bar
|
||||||
|
self.mount_progress_bar(format!("Uploading {}…", file.path.display()));
|
||||||
|
// Get remote path
|
||||||
|
let file_name: String = file.name();
|
||||||
|
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
|
||||||
|
let remote_file_name: PathBuf = match dst_name {
|
||||||
|
Some(s) => PathBuf::from(s.as_str()),
|
||||||
|
None => PathBuf::from(file_name.as_str()),
|
||||||
|
};
|
||||||
|
remote_path.push(remote_file_name);
|
||||||
|
// Send
|
||||||
|
let result = self.filetransfer_send_one(file, remote_path.as_path(), file_name);
|
||||||
|
// Umount progress bar
|
||||||
|
self.umount_progress_bar();
|
||||||
|
// Return result
|
||||||
|
result.map_err(|x| x.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a `TransferPayload` of type `Any`
|
||||||
|
fn filetransfer_send_any(
|
||||||
|
&mut self,
|
||||||
|
entry: &File,
|
||||||
|
curr_remote_path: &Path,
|
||||||
|
dst_name: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Reset states
|
||||||
|
self.transfer.reset();
|
||||||
|
// Calculate total size of transfer
|
||||||
|
let total_transfer_size: usize = self.get_total_transfer_size(entry, true);
|
||||||
|
self.transfer.full.init(total_transfer_size);
|
||||||
|
// Mount progress bar
|
||||||
|
self.mount_progress_bar(format!("Uploading {}…", entry.path().display()));
|
||||||
|
// Send recurse
|
||||||
|
let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
|
||||||
|
// Umount progress bar
|
||||||
|
self.umount_progress_bar();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send transfer queue entries to remote
|
||||||
|
fn filetransfer_send_transfer_queue(
|
||||||
|
&mut self,
|
||||||
|
entries: &[(File, PathBuf)],
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Reset states
|
||||||
|
self.transfer.reset();
|
||||||
|
// Calculate total size of transfer
|
||||||
|
let total_transfer_size: usize = entries
|
||||||
|
.iter()
|
||||||
|
.map(|(x, _)| self.get_total_transfer_size(x, true))
|
||||||
|
.sum();
|
||||||
|
self.transfer.full.init(total_transfer_size);
|
||||||
|
// Mount progress bar
|
||||||
|
self.mount_progress_bar(format!("Uploading {} entries…", entries.len()));
|
||||||
|
// Send recurse
|
||||||
|
let result = entries
|
||||||
|
.iter()
|
||||||
|
.map(|(x, remote)| self.filetransfer_send_recurse(x, remote, None))
|
||||||
|
.find(|x| x.is_err())
|
||||||
|
.unwrap_or(Ok(()));
|
||||||
|
// Umount progress bar
|
||||||
|
self.umount_progress_bar();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filetransfer_send_recurse(
|
||||||
|
&mut self,
|
||||||
|
entry: &File,
|
||||||
|
curr_remote_path: &Path,
|
||||||
|
dst_name: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Write popup
|
||||||
|
let file_name = entry.name();
|
||||||
|
// Get remote path
|
||||||
|
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
|
||||||
|
let remote_file_name: PathBuf = match dst_name {
|
||||||
|
Some(s) => PathBuf::from(s.as_str()),
|
||||||
|
None => PathBuf::from(file_name.as_str()),
|
||||||
|
};
|
||||||
|
remote_path.push(remote_file_name);
|
||||||
|
// Match entry
|
||||||
|
let result: Result<(), String> = if entry.is_dir() {
|
||||||
|
// Create directory on remote first
|
||||||
|
match self
|
||||||
|
.browser
|
||||||
|
.remote_pane_mut()
|
||||||
|
.fs
|
||||||
|
.mkdir_ex(remote_path.as_path(), true)
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Created directory \"{}\"", remote_path.display()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Failed to create directory \"{}\": {}",
|
||||||
|
remote_path.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return Err(err.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Get files in dir
|
||||||
|
match self.browser.local_pane_mut().fs.list_dir(entry.path()) {
|
||||||
|
Ok(entries) => {
|
||||||
|
// Iterate over files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
// If aborted; break
|
||||||
|
if self.transfer.aborted() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Send entry; name is always None after first call
|
||||||
|
self.filetransfer_send_recurse(entry, remote_path.as_path(), None)?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not scan directory \"{}\": {}",
|
||||||
|
entry.path().display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Err(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match self.filetransfer_send_one(entry, remote_path.as_path(), file_name) {
|
||||||
|
Err(err) => {
|
||||||
|
// If transfer was abrupted or there was an IO error on remote, remove file
|
||||||
|
if matches!(
|
||||||
|
err,
|
||||||
|
TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_)
|
||||||
|
) {
|
||||||
|
// Stat file on remote and remove it if exists
|
||||||
|
match self
|
||||||
|
.browser
|
||||||
|
.remote_pane_mut()
|
||||||
|
.fs
|
||||||
|
.stat(remote_path.as_path())
|
||||||
|
{
|
||||||
|
Err(err) => self.log(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not remove created file {}: {}",
|
||||||
|
remote_path.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Ok(entry) => {
|
||||||
|
if let Err(err) = self.browser.remote_pane_mut().fs.remove(&entry) {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not remove created file {}: {}",
|
||||||
|
remote_path.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err.to_string())
|
||||||
|
}
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Scan dir on remote
|
||||||
|
self.reload_remote_dir();
|
||||||
|
// If aborted; show popup
|
||||||
|
if self.transfer.aborted() {
|
||||||
|
// Log abort
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Warn,
|
||||||
|
format!("Upload aborted for \"{}\"!", entry.path().display()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send host_bridge file and write it to remote path
|
||||||
|
fn filetransfer_send_one(
|
||||||
|
&mut self,
|
||||||
|
host_bridge: &File,
|
||||||
|
remote: &Path,
|
||||||
|
file_name: String,
|
||||||
|
) -> Result<(), TransferErrorReason> {
|
||||||
|
// Sync file size and attributes before transfer
|
||||||
|
let metadata = self
|
||||||
|
.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.stat(host_bridge.path.as_path())
|
||||||
|
.map_err(TransferErrorReason::HostError)
|
||||||
|
.map(|x| x.metadata().clone())?;
|
||||||
|
|
||||||
|
if !self.has_file_changed(remote, &metadata, false) {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"file {} won't be transferred since hasn't changed",
|
||||||
|
host_bridge.path().display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
self.transfer.full.update_progress(metadata.size as usize);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// Upload file
|
||||||
|
// Open host_bridge file for reading
|
||||||
|
let reader = self
|
||||||
|
.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.open_file(host_bridge.path.as_path())
|
||||||
|
.map_err(TransferErrorReason::HostError)?;
|
||||||
|
// Open remote file for writing
|
||||||
|
let writer = self
|
||||||
|
.browser
|
||||||
|
.remote_pane_mut()
|
||||||
|
.fs
|
||||||
|
.create_file(remote, &metadata)
|
||||||
|
.map_err(TransferErrorReason::RemoteHostError)?;
|
||||||
|
|
||||||
|
self.filetransfer_send_one_with_stream(host_bridge, remote, file_name, reader, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send file to remote using stream
|
||||||
|
fn filetransfer_send_one_with_stream(
|
||||||
|
&mut self,
|
||||||
|
host: &File,
|
||||||
|
remote: &Path,
|
||||||
|
file_name: String,
|
||||||
|
mut reader: Box<dyn Read + Send>,
|
||||||
|
mut writer: Box<dyn Write + Send>,
|
||||||
|
) -> Result<(), TransferErrorReason> {
|
||||||
|
// Write file
|
||||||
|
let file_size = self
|
||||||
|
.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.stat(host.path())
|
||||||
|
.map_err(TransferErrorReason::HostError)
|
||||||
|
.map(|x| x.metadata().size as usize)?;
|
||||||
|
// Init transfer
|
||||||
|
self.transfer.partial.init(file_size);
|
||||||
|
|
||||||
|
// Write remote file
|
||||||
|
let mut total_bytes_written: usize = 0;
|
||||||
|
let mut last_progress_val: f64 = 0.0;
|
||||||
|
let mut last_input_event_fetch: Option<Instant> = None;
|
||||||
|
// While the entire file hasn't been completely written,
|
||||||
|
// Or filetransfer has been aborted
|
||||||
|
while total_bytes_written < file_size && !self.transfer.aborted() {
|
||||||
|
// Handle input events (each 500ms) or if never fetched before
|
||||||
|
if last_input_event_fetch.is_none()
|
||||||
|
|| last_input_event_fetch
|
||||||
|
.unwrap_or_else(Instant::now)
|
||||||
|
.elapsed()
|
||||||
|
.as_millis()
|
||||||
|
>= 500
|
||||||
|
{
|
||||||
|
// Read events
|
||||||
|
self.tick();
|
||||||
|
// Reset instant
|
||||||
|
last_input_event_fetch = Some(Instant::now());
|
||||||
|
}
|
||||||
|
// Read till you can
|
||||||
|
let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE];
|
||||||
|
let delta: usize = match reader.read(&mut buffer) {
|
||||||
|
Ok(bytes_read) => {
|
||||||
|
total_bytes_written += bytes_read;
|
||||||
|
if bytes_read == 0 {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
let mut delta: usize = 0;
|
||||||
|
while delta < bytes_read {
|
||||||
|
// Write bytes
|
||||||
|
match writer.write(&buffer[delta..bytes_read]) {
|
||||||
|
Ok(bytes) => {
|
||||||
|
delta += bytes;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(TransferErrorReason::RemoteIoError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(TransferErrorReason::HostIoError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Increase progress
|
||||||
|
self.transfer.partial.update_progress(delta);
|
||||||
|
self.transfer.full.update_progress(delta);
|
||||||
|
// Draw only if a significant progress has been made (performance improvement)
|
||||||
|
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
|
||||||
|
// Draw
|
||||||
|
self.update_progress_bar(format!("Uploading \"{file_name}\"…"));
|
||||||
|
self.view();
|
||||||
|
last_progress_val = self.transfer.partial.calc_progress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Finalize stream
|
||||||
|
if let Err(err) = self.browser.remote_pane_mut().fs.finalize_write(writer) {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Warn,
|
||||||
|
format!("Could not finalize remote stream: \"{err}\""),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// if upload was abrupted, return error
|
||||||
|
if self.transfer.aborted() {
|
||||||
|
return Err(TransferErrorReason::Abrupted);
|
||||||
|
}
|
||||||
|
// set stat
|
||||||
|
if let Err(err) = self
|
||||||
|
.browser
|
||||||
|
.remote_pane_mut()
|
||||||
|
.fs
|
||||||
|
.setstat(remote, host.metadata())
|
||||||
|
{
|
||||||
|
error!("failed to set stat for {}: {}", remote.display(), err);
|
||||||
|
}
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||||
|
host.path.display(),
|
||||||
|
remote.display(),
|
||||||
|
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||||
|
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recv fs entry from remote.
|
||||||
|
/// If dst_name is Some, entry will be saved with a different name.
|
||||||
|
/// If entry is a directory, this applies to directory only
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn filetransfer_recv(
|
||||||
|
&mut self,
|
||||||
|
payload: TransferPayload,
|
||||||
|
host_bridge_path: &Path,
|
||||||
|
dst_name: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let result = match payload {
|
||||||
|
TransferPayload::Any(ref entry) => {
|
||||||
|
self.filetransfer_recv_any(entry, host_bridge_path, dst_name)
|
||||||
|
}
|
||||||
|
TransferPayload::File(ref file) => self.filetransfer_recv_file(file, host_bridge_path),
|
||||||
|
TransferPayload::TransferQueue(ref entries) => {
|
||||||
|
self.filetransfer_recv_transfer_queue(entries)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Notify
|
||||||
|
match &result {
|
||||||
|
Ok(_) => {
|
||||||
|
self.notify_transfer_completed(&payload);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.notify_transfer_error(e.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recv fs entry from remote.
|
||||||
|
/// If dst_name is Some, entry will be saved with a different name.
|
||||||
|
/// If entry is a directory, this applies to directory only
|
||||||
|
fn filetransfer_recv_any(
|
||||||
|
&mut self,
|
||||||
|
entry: &File,
|
||||||
|
host_path: &Path,
|
||||||
|
dst_name: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Reset states
|
||||||
|
self.transfer.reset();
|
||||||
|
// Calculate total transfer size
|
||||||
|
let total_transfer_size: usize = self.get_total_transfer_size(entry, false);
|
||||||
|
self.transfer.full.init(total_transfer_size);
|
||||||
|
// Mount progress bar
|
||||||
|
self.mount_progress_bar(format!("Downloading {}…", entry.path().display()));
|
||||||
|
// Receive
|
||||||
|
let result = self.filetransfer_recv_recurse(entry, host_path, dst_name);
|
||||||
|
// Umount progress bar
|
||||||
|
self.umount_progress_bar();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive a single file from remote.
|
||||||
|
fn filetransfer_recv_file(
|
||||||
|
&mut self,
|
||||||
|
entry: &File,
|
||||||
|
host_bridge_path: &Path,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Reset states
|
||||||
|
self.transfer.reset();
|
||||||
|
// Calculate total transfer size
|
||||||
|
let total_transfer_size: usize = entry.metadata.size as usize;
|
||||||
|
self.transfer.full.init(total_transfer_size);
|
||||||
|
// Mount progress bar
|
||||||
|
self.mount_progress_bar(format!("Downloading {}…", entry.path.display()));
|
||||||
|
// Receive
|
||||||
|
let result = self.filetransfer_recv_one(host_bridge_path, entry, entry.name());
|
||||||
|
// Umount progress bar
|
||||||
|
self.umount_progress_bar();
|
||||||
|
// Return result
|
||||||
|
result.map_err(|x| x.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive transfer queue from remote
|
||||||
|
fn filetransfer_recv_transfer_queue(
|
||||||
|
&mut self,
|
||||||
|
entries: &[(File, PathBuf)],
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Reset states
|
||||||
|
self.transfer.reset();
|
||||||
|
// Calculate total size of transfer
|
||||||
|
let total_transfer_size: usize = entries
|
||||||
|
.iter()
|
||||||
|
.map(|(x, _)| self.get_total_transfer_size(x, false))
|
||||||
|
.sum();
|
||||||
|
self.transfer.full.init(total_transfer_size);
|
||||||
|
// Mount progress bar
|
||||||
|
self.mount_progress_bar(format!("Downloading {} entries…", entries.len()));
|
||||||
|
// Send recurse
|
||||||
|
let result = entries
|
||||||
|
.iter()
|
||||||
|
.map(|(x, path)| self.filetransfer_recv_recurse(x, path, None))
|
||||||
|
.find(|x| x.is_err())
|
||||||
|
.unwrap_or(Ok(()));
|
||||||
|
// Umount progress bar
|
||||||
|
self.umount_progress_bar();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filetransfer_recv_recurse(
|
||||||
|
&mut self,
|
||||||
|
entry: &File,
|
||||||
|
host_bridge_path: &Path,
|
||||||
|
dst_name: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Write popup
|
||||||
|
let file_name = entry.name();
|
||||||
|
// Match entry
|
||||||
|
let result: Result<(), String> = if entry.is_dir() {
|
||||||
|
// Get dir name
|
||||||
|
let mut host_bridge_dir_path: PathBuf = PathBuf::from(host_bridge_path);
|
||||||
|
match dst_name {
|
||||||
|
Some(name) => host_bridge_dir_path.push(name),
|
||||||
|
None => host_bridge_dir_path.push(entry.name()),
|
||||||
|
}
|
||||||
|
// Create directory on host_bridge
|
||||||
|
match self
|
||||||
|
.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.mkdir_ex(host_bridge_dir_path.as_path(), true)
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
// Apply file mode to directory
|
||||||
|
if let Err(err) = self
|
||||||
|
.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.setstat(host_bridge_dir_path.as_path(), entry.metadata())
|
||||||
|
{
|
||||||
|
self.log(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not set stat to directory {:?} to \"{}\": {}",
|
||||||
|
entry.metadata(),
|
||||||
|
host_bridge_dir_path.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Created directory \"{}\"", host_bridge_dir_path.display()),
|
||||||
|
);
|
||||||
|
// Get files in dir from remote
|
||||||
|
match self.browser.remote_pane_mut().fs.list_dir(entry.path()) {
|
||||||
|
Ok(entries) => {
|
||||||
|
// Iterate over files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
// If transfer has been aborted; break
|
||||||
|
if self.transfer.aborted() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Receive entry; name is always None after first call
|
||||||
|
// Local path becomes host_bridge_dir_path
|
||||||
|
self.filetransfer_recv_recurse(
|
||||||
|
entry,
|
||||||
|
host_bridge_dir_path.as_path(),
|
||||||
|
None,
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not scan directory \"{}\": {}",
|
||||||
|
entry.path().display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Err(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Failed to create directory \"{}\": {}",
|
||||||
|
host_bridge_dir_path.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Err(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Get host_bridge file
|
||||||
|
let mut host_bridge_file_path: PathBuf = PathBuf::from(host_bridge_path);
|
||||||
|
let host_bridge_file_name: String = match dst_name {
|
||||||
|
Some(n) => n,
|
||||||
|
None => entry.name(),
|
||||||
|
};
|
||||||
|
host_bridge_file_path.push(host_bridge_file_name.as_str());
|
||||||
|
// Download file
|
||||||
|
if let Err(err) =
|
||||||
|
self.filetransfer_recv_one(host_bridge_file_path.as_path(), entry, file_name)
|
||||||
|
{
|
||||||
|
// If transfer was abrupted or there was an IO error on remote, remove file
|
||||||
|
if matches!(
|
||||||
|
err,
|
||||||
|
TransferErrorReason::Abrupted | TransferErrorReason::HostIoError(_)
|
||||||
|
) {
|
||||||
|
// Stat file
|
||||||
|
match self
|
||||||
|
.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.stat(host_bridge_file_path.as_path())
|
||||||
|
{
|
||||||
|
Err(err) => self.log(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not remove created file {}: {}",
|
||||||
|
host_bridge_file_path.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Ok(entry) => {
|
||||||
|
if let Err(err) = self.browser.local_pane_mut().fs.remove(&entry) {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not remove created file {}: {}",
|
||||||
|
host_bridge_file_path.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err.to_string())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Reload directory on host_bridge
|
||||||
|
self.reload_host_bridge_dir();
|
||||||
|
// if aborted; show alert
|
||||||
|
if self.transfer.aborted() {
|
||||||
|
// Log abort
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Warn,
|
||||||
|
format!("Download aborted for \"{}\"!", entry.path().display()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive file from remote and write it to host_bridge path
|
||||||
|
fn filetransfer_recv_one(
|
||||||
|
&mut self,
|
||||||
|
host_bridge: &Path,
|
||||||
|
remote: &File,
|
||||||
|
file_name: String,
|
||||||
|
) -> Result<(), TransferErrorReason> {
|
||||||
|
// check if files are equal (in case, don't transfer)
|
||||||
|
if !self.has_file_changed(host_bridge, remote.metadata(), true) {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"file {} won't be transferred since hasn't changed",
|
||||||
|
remote.path().display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
self.transfer
|
||||||
|
.full
|
||||||
|
.update_progress(remote.metadata().size as usize);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open host_bridge file for writing
|
||||||
|
let writer = self
|
||||||
|
.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.create_file(host_bridge, &remote.metadata)
|
||||||
|
.map_err(TransferErrorReason::HostError)?;
|
||||||
|
// Open remote file for reading
|
||||||
|
let reader = self
|
||||||
|
.browser
|
||||||
|
.remote_pane_mut()
|
||||||
|
.fs
|
||||||
|
.open_file(remote.path.as_path())
|
||||||
|
.map_err(TransferErrorReason::RemoteHostError)?;
|
||||||
|
|
||||||
|
self.filetransfer_recv_one_with_stream(host_bridge, remote, file_name, reader, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive an `File` from remote using stream
|
||||||
|
fn filetransfer_recv_one_with_stream(
|
||||||
|
&mut self,
|
||||||
|
host_bridge: &Path,
|
||||||
|
remote: &File,
|
||||||
|
file_name: String,
|
||||||
|
mut reader: Box<dyn Read + Send>,
|
||||||
|
mut writer: Box<dyn Write + Send>,
|
||||||
|
) -> Result<(), TransferErrorReason> {
|
||||||
|
let mut total_bytes_written: usize = 0;
|
||||||
|
// Init transfer
|
||||||
|
self.transfer.partial.init(remote.metadata.size as usize);
|
||||||
|
// Write host_bridge file
|
||||||
|
let mut last_progress_val: f64 = 0.0;
|
||||||
|
let mut last_input_event_fetch: Option<Instant> = None;
|
||||||
|
// While the entire file hasn't been completely read,
|
||||||
|
// Or filetransfer has been aborted
|
||||||
|
while total_bytes_written < remote.metadata.size as usize && !self.transfer.aborted() {
|
||||||
|
// Handle input events (each 500 ms) or is None
|
||||||
|
if last_input_event_fetch.is_none()
|
||||||
|
|| last_input_event_fetch
|
||||||
|
.unwrap_or_else(Instant::now)
|
||||||
|
.elapsed()
|
||||||
|
.as_millis()
|
||||||
|
>= 500
|
||||||
|
{
|
||||||
|
// Read events
|
||||||
|
self.tick();
|
||||||
|
// Reset instant
|
||||||
|
last_input_event_fetch = Some(Instant::now());
|
||||||
|
}
|
||||||
|
// Read till you can
|
||||||
|
let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE];
|
||||||
|
let delta: usize = match reader.read(&mut buffer) {
|
||||||
|
Ok(bytes_read) => {
|
||||||
|
total_bytes_written += bytes_read;
|
||||||
|
if bytes_read == 0 {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
let mut delta: usize = 0;
|
||||||
|
while delta < bytes_read {
|
||||||
|
// Write bytes
|
||||||
|
match writer.write(&buffer[delta..bytes_read]) {
|
||||||
|
Ok(bytes) => delta += bytes,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(TransferErrorReason::HostIoError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(TransferErrorReason::RemoteIoError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Set progress
|
||||||
|
self.transfer.partial.update_progress(delta);
|
||||||
|
self.transfer.full.update_progress(delta);
|
||||||
|
// Draw only if a significant progress has been made (performance improvement)
|
||||||
|
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
|
||||||
|
// Draw
|
||||||
|
self.update_progress_bar(format!("Downloading \"{file_name}\""));
|
||||||
|
self.view();
|
||||||
|
last_progress_val = self.transfer.partial.calc_progress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If download was abrupted, return Error
|
||||||
|
if self.transfer.aborted() {
|
||||||
|
return Err(TransferErrorReason::Abrupted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize write
|
||||||
|
self.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.finalize_write(writer)
|
||||||
|
.map_err(TransferErrorReason::HostError)?;
|
||||||
|
|
||||||
|
// Apply file mode to file
|
||||||
|
if let Err(err) = self
|
||||||
|
.browser
|
||||||
|
.local_pane_mut()
|
||||||
|
.fs
|
||||||
|
.setstat(host_bridge, remote.metadata())
|
||||||
|
{
|
||||||
|
self.log(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not set stat to file {:?} to \"{}\": {}",
|
||||||
|
remote.metadata(),
|
||||||
|
host_bridge.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Log
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||||
|
remote.path.display(),
|
||||||
|
host_bridge.display(),
|
||||||
|
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||||
|
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransferActivity
|
//! ## FileTransferActivity
|
||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
// locals
|
||||||
// externals
|
// externals
|
||||||
@@ -12,6 +12,7 @@ use super::actions::walkdir::WalkdirError;
|
|||||||
use super::browser::{FileExplorerTab, FoundExplorerTab};
|
use super::browser::{FileExplorerTab, FoundExplorerTab};
|
||||||
use super::{
|
use super::{
|
||||||
ExitReason, FileTransferActivity, Id, MarkQueue, Msg, TransferMsg, TransferOpts, UiMsg,
|
ExitReason, FileTransferActivity, Id, MarkQueue, Msg, TransferMsg, TransferOpts, UiMsg,
|
||||||
|
ui_result,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Update<Msg> for FileTransferActivity {
|
impl Update<Msg> for FileTransferActivity {
|
||||||
@@ -40,13 +41,12 @@ impl FileTransferActivity {
|
|||||||
TransferMsg::Chmod(mode) => {
|
TransferMsg::Chmod(mode) => {
|
||||||
self.umount_chmod();
|
self.umount_chmod();
|
||||||
self.mount_blocking_wait("Applying new file mode…");
|
self.mount_blocking_wait("Applying new file mode…");
|
||||||
match self.browser.tab() {
|
// Skip chmod on Windows localhost
|
||||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge
|
if !(self.is_local_tab()
|
||||||
if self.host_bridge.is_localhost() && cfg!(windows) => {}
|
&& self.browser.local_pane().fs.is_localhost()
|
||||||
FileExplorerTab::HostBridge => self.action_local_chmod(mode),
|
&& cfg!(windows))
|
||||||
FileExplorerTab::FindHostBridge => self.action_find_local_chmod(mode),
|
{
|
||||||
FileExplorerTab::Remote => self.action_remote_chmod(mode),
|
self.action_chmod(mode);
|
||||||
FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode),
|
|
||||||
}
|
}
|
||||||
self.umount_wait();
|
self.umount_wait();
|
||||||
self.update_browser_file_list();
|
self.update_browser_file_list();
|
||||||
@@ -54,11 +54,7 @@ impl FileTransferActivity {
|
|||||||
TransferMsg::CopyFileTo(dest) => {
|
TransferMsg::CopyFileTo(dest) => {
|
||||||
self.umount_copy();
|
self.umount_copy();
|
||||||
self.mount_blocking_wait("Copying file(s)…");
|
self.mount_blocking_wait("Copying file(s)…");
|
||||||
match self.browser.tab() {
|
self.action_copy(dest);
|
||||||
FileExplorerTab::HostBridge => self.action_local_copy(dest),
|
|
||||||
FileExplorerTab::Remote => self.action_remote_copy(dest),
|
|
||||||
_ => panic!("Found tab doesn't support COPY"),
|
|
||||||
}
|
|
||||||
self.umount_wait();
|
self.umount_wait();
|
||||||
// Reload files
|
// Reload files
|
||||||
self.update_browser_file_list()
|
self.update_browser_file_list()
|
||||||
@@ -66,11 +62,7 @@ impl FileTransferActivity {
|
|||||||
TransferMsg::CreateSymlink(name) => {
|
TransferMsg::CreateSymlink(name) => {
|
||||||
self.umount_symlink();
|
self.umount_symlink();
|
||||||
self.mount_blocking_wait("Creating symlink…");
|
self.mount_blocking_wait("Creating symlink…");
|
||||||
match self.browser.tab() {
|
self.action_symlink(name);
|
||||||
FileExplorerTab::HostBridge => self.action_local_symlink(name),
|
|
||||||
FileExplorerTab::Remote => self.action_remote_symlink(name),
|
|
||||||
_ => panic!("Found tab doesn't support SYMLINK"),
|
|
||||||
}
|
|
||||||
self.umount_wait();
|
self.umount_wait();
|
||||||
// Reload files
|
// Reload files
|
||||||
self.update_browser_file_list()
|
self.update_browser_file_list()
|
||||||
@@ -79,15 +71,14 @@ impl FileTransferActivity {
|
|||||||
self.umount_radio_delete();
|
self.umount_radio_delete();
|
||||||
self.mount_blocking_wait("Removing file(s)…");
|
self.mount_blocking_wait("Removing file(s)…");
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::HostBridge => self.action_local_delete(),
|
FileExplorerTab::HostBridge | FileExplorerTab::Remote => {
|
||||||
FileExplorerTab::Remote => self.action_remote_delete(),
|
self.action_delete();
|
||||||
|
}
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||||
// Get entry
|
|
||||||
self.action_find_delete();
|
self.action_find_delete();
|
||||||
// Delete entries
|
// Remove deleted entries from the find-result list
|
||||||
match self.app.state(&Id::ExplorerFind) {
|
match self.app.state(&Id::ExplorerFind) {
|
||||||
Ok(State::One(StateValue::Usize(idx))) => {
|
Ok(State::One(StateValue::Usize(idx))) => {
|
||||||
// Reload entries
|
|
||||||
self.found_mut().unwrap().del_entry(idx);
|
self.found_mut().unwrap().del_entry(idx);
|
||||||
}
|
}
|
||||||
Ok(State::Vec(values)) => {
|
Ok(State::Vec(values)) => {
|
||||||
@@ -106,31 +97,19 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
self.umount_wait();
|
self.umount_wait();
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.browser.tab() {
|
self.update_browser_file_list();
|
||||||
FileExplorerTab::HostBridge => self.update_host_bridge_filelist(),
|
|
||||||
FileExplorerTab::Remote => self.update_remote_filelist(),
|
|
||||||
FileExplorerTab::FindHostBridge => self.update_host_bridge_filelist(),
|
|
||||||
FileExplorerTab::FindRemote => self.update_remote_filelist(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::HostBridge => {
|
TransferMsg::EnterDirectory
|
||||||
if let Some(entry) = self.get_local_selected_file() {
|
if self.browser.tab() == FileExplorerTab::HostBridge
|
||||||
self.action_submit_local(entry);
|
|| self.browser.tab() == FileExplorerTab::Remote =>
|
||||||
|
{
|
||||||
|
if let Some(entry) = self.get_selected_file() {
|
||||||
|
self.action_submit(entry);
|
||||||
// Update file list if sync
|
// Update file list if sync
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.update_remote_filelist();
|
self.update_browser_file_list_swapped();
|
||||||
}
|
}
|
||||||
self.update_host_bridge_filelist();
|
self.update_browser_file_list();
|
||||||
}
|
|
||||||
}
|
|
||||||
TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => {
|
|
||||||
if let Some(entry) = self.get_remote_selected_file() {
|
|
||||||
self.action_submit_remote(entry);
|
|
||||||
// Update file list if sync
|
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.update_host_bridge_filelist();
|
|
||||||
}
|
|
||||||
self.update_remote_filelist();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TransferMsg::EnterDirectory => {
|
TransferMsg::EnterDirectory => {
|
||||||
@@ -145,22 +124,13 @@ impl FileTransferActivity {
|
|||||||
self.update_browser_file_list()
|
self.update_browser_file_list()
|
||||||
}
|
}
|
||||||
TransferMsg::ExecuteCmd(cmd) => {
|
TransferMsg::ExecuteCmd(cmd) => {
|
||||||
// Exec command
|
self.action_exec_cmd(cmd);
|
||||||
match self.browser.tab() {
|
|
||||||
FileExplorerTab::HostBridge => self.action_local_exec(cmd),
|
|
||||||
FileExplorerTab::Remote => self.action_remote_exec(cmd),
|
|
||||||
_ => panic!("Found tab doesn't support EXEC"),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
TransferMsg::GetFileSize => {
|
TransferMsg::GetFileSize => {
|
||||||
self.action_get_file_size();
|
self.action_get_file_size();
|
||||||
}
|
}
|
||||||
TransferMsg::GoTo(dir) => {
|
TransferMsg::GoTo(dir) => {
|
||||||
match self.browser.tab() {
|
self.action_change_dir(dir);
|
||||||
FileExplorerTab::HostBridge => self.action_change_local_dir(dir),
|
|
||||||
FileExplorerTab::Remote => self.action_change_remote_dir(dir),
|
|
||||||
_ => panic!("Found tab doesn't support GOTO"),
|
|
||||||
}
|
|
||||||
// Umount
|
// Umount
|
||||||
self.umount_goto();
|
self.umount_goto();
|
||||||
// Reload files if sync
|
// Reload files if sync
|
||||||
@@ -171,56 +141,24 @@ impl FileTransferActivity {
|
|||||||
self.update_browser_file_list()
|
self.update_browser_file_list()
|
||||||
}
|
}
|
||||||
TransferMsg::GoToParentDirectory => {
|
TransferMsg::GoToParentDirectory => {
|
||||||
match self.browser.tab() {
|
self.action_go_to_upper_dir();
|
||||||
FileExplorerTab::HostBridge => {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.action_go_to_local_upper_dir();
|
self.update_browser_file_list_swapped();
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.update_remote_filelist();
|
|
||||||
}
|
|
||||||
// Reload file list component
|
|
||||||
self.update_host_bridge_filelist()
|
|
||||||
}
|
|
||||||
FileExplorerTab::Remote => {
|
|
||||||
self.action_go_to_remote_upper_dir();
|
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.update_host_bridge_filelist();
|
|
||||||
}
|
|
||||||
// Reload file list component
|
|
||||||
self.update_remote_filelist()
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
self.update_browser_file_list();
|
||||||
}
|
}
|
||||||
TransferMsg::GoToPreviousDirectory => {
|
TransferMsg::GoToPreviousDirectory => {
|
||||||
match self.browser.tab() {
|
self.action_go_to_previous_dir();
|
||||||
FileExplorerTab::HostBridge => {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.action_go_to_previous_local_dir();
|
self.update_browser_file_list_swapped();
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.update_remote_filelist();
|
|
||||||
}
|
|
||||||
// Reload file list component
|
|
||||||
self.update_host_bridge_filelist()
|
|
||||||
}
|
|
||||||
FileExplorerTab::Remote => {
|
|
||||||
self.action_go_to_previous_remote_dir();
|
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
|
||||||
self.update_host_bridge_filelist();
|
|
||||||
}
|
|
||||||
// Reload file list component
|
|
||||||
self.update_remote_filelist()
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
self.update_browser_file_list();
|
||||||
}
|
}
|
||||||
TransferMsg::InitFuzzySearch => {
|
TransferMsg::InitFuzzySearch => {
|
||||||
// Mount wait
|
// Mount wait
|
||||||
self.mount_walkdir_wait();
|
self.mount_walkdir_wait();
|
||||||
// Find
|
// Find
|
||||||
let res: Result<Vec<File>, WalkdirError> = match self.browser.tab() {
|
let res: Result<Vec<File>, WalkdirError> = self.action_walkdir();
|
||||||
FileExplorerTab::HostBridge => self.action_walkdir_local(),
|
|
||||||
FileExplorerTab::Remote => self.action_walkdir_remote(),
|
|
||||||
_ => panic!("Trying to search for files, while already in a find result"),
|
|
||||||
};
|
|
||||||
// Umount wait
|
// Umount wait
|
||||||
self.umount_wait();
|
self.umount_wait();
|
||||||
// Match result
|
// Match result
|
||||||
@@ -266,36 +204,28 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TransferMsg::Mkdir(dir) => {
|
TransferMsg::Mkdir(dir) => {
|
||||||
match self.browser.tab() {
|
self.action_mkdir(dir);
|
||||||
FileExplorerTab::HostBridge => self.action_local_mkdir(dir),
|
|
||||||
FileExplorerTab::Remote => self.action_remote_mkdir(dir),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self.umount_mkdir();
|
self.umount_mkdir();
|
||||||
// Reload files
|
// Reload files
|
||||||
self.update_browser_file_list()
|
self.update_browser_file_list()
|
||||||
}
|
}
|
||||||
TransferMsg::NewFile(name) => {
|
TransferMsg::NewFile(name) => {
|
||||||
match self.browser.tab() {
|
self.action_newfile(name);
|
||||||
FileExplorerTab::HostBridge => self.action_local_newfile(name),
|
|
||||||
FileExplorerTab::Remote => self.action_remote_newfile(name),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self.umount_newfile();
|
self.umount_newfile();
|
||||||
// Reload files
|
// Reload files
|
||||||
self.update_browser_file_list()
|
self.update_browser_file_list()
|
||||||
}
|
}
|
||||||
TransferMsg::OpenFile => match self.browser.tab() {
|
TransferMsg::OpenFile => match self.browser.tab() {
|
||||||
FileExplorerTab::HostBridge => self.action_open_local(),
|
FileExplorerTab::HostBridge | FileExplorerTab::Remote => self.action_open(),
|
||||||
FileExplorerTab::Remote => self.action_open_remote(),
|
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||||
self.action_find_open()
|
self.action_find_open()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TransferMsg::OpenFileWith(prog) => {
|
TransferMsg::OpenFileWith(prog) => {
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::HostBridge => self.action_local_open_with(&prog),
|
FileExplorerTab::HostBridge | FileExplorerTab::Remote => {
|
||||||
FileExplorerTab::Remote => self.action_remote_open_with(&prog),
|
self.action_open_with(&prog)
|
||||||
|
}
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||||
self.action_find_open_with(&prog)
|
self.action_find_open_with(&prog)
|
||||||
}
|
}
|
||||||
@@ -314,11 +244,7 @@ impl FileTransferActivity {
|
|||||||
TransferMsg::RenameFile(dest) => {
|
TransferMsg::RenameFile(dest) => {
|
||||||
self.umount_rename();
|
self.umount_rename();
|
||||||
self.mount_blocking_wait("Moving file(s)…");
|
self.mount_blocking_wait("Moving file(s)…");
|
||||||
match self.browser.tab() {
|
self.action_rename(dest);
|
||||||
FileExplorerTab::HostBridge => self.action_local_rename(dest),
|
|
||||||
FileExplorerTab::Remote => self.action_remote_rename(dest),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self.umount_wait();
|
self.umount_wait();
|
||||||
// Reload files
|
// Reload files
|
||||||
self.update_browser_file_list()
|
self.update_browser_file_list()
|
||||||
@@ -335,10 +261,10 @@ impl FileTransferActivity {
|
|||||||
TransferMsg::SaveFileAs(dest) => {
|
TransferMsg::SaveFileAs(dest) => {
|
||||||
self.umount_saveas();
|
self.umount_saveas();
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::HostBridge => self.action_local_saveas(dest),
|
FileExplorerTab::HostBridge | FileExplorerTab::Remote => {
|
||||||
FileExplorerTab::Remote => self.action_remote_saveas(dest),
|
self.action_saveas(dest)
|
||||||
|
}
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||||
// Get entry
|
|
||||||
self.action_find_transfer(TransferOpts::default().save_as(Some(dest)));
|
self.action_find_transfer(TransferOpts::default().save_as(Some(dest)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,8 +277,9 @@ impl FileTransferActivity {
|
|||||||
TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index),
|
TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index),
|
||||||
TransferMsg::TransferFile => {
|
TransferMsg::TransferFile => {
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::HostBridge => self.action_local_send(),
|
FileExplorerTab::HostBridge | FileExplorerTab::Remote => {
|
||||||
FileExplorerTab::Remote => self.action_remote_recv(),
|
self.action_transfer_file()
|
||||||
|
}
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||||
self.action_find_transfer(TransferOpts::default())
|
self.action_find_transfer(TransferOpts::default())
|
||||||
}
|
}
|
||||||
@@ -399,13 +326,13 @@ impl FileTransferActivity {
|
|||||||
// Set focus
|
// Set focus
|
||||||
match new_tab {
|
match new_tab {
|
||||||
FileExplorerTab::HostBridge => {
|
FileExplorerTab::HostBridge => {
|
||||||
assert!(self.app.active(&Id::ExplorerHostBridge).is_ok())
|
ui_result(self.app.active(&Id::ExplorerHostBridge));
|
||||||
}
|
}
|
||||||
FileExplorerTab::Remote => {
|
FileExplorerTab::Remote => {
|
||||||
assert!(self.app.active(&Id::ExplorerRemote).is_ok())
|
ui_result(self.app.active(&Id::ExplorerRemote));
|
||||||
}
|
}
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||||
assert!(self.app.active(&Id::ExplorerFind).is_ok())
|
ui_result(self.app.active(&Id::ExplorerFind));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.browser.change_tab(new_tab);
|
self.browser.change_tab(new_tab);
|
||||||
@@ -476,10 +403,10 @@ impl FileTransferActivity {
|
|||||||
self.update_find_list();
|
self.update_find_list();
|
||||||
}
|
}
|
||||||
UiMsg::GoToTransferQueue => {
|
UiMsg::GoToTransferQueue => {
|
||||||
assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok());
|
ui_result(self.app.active(&Id::TransferQueueHostBridge));
|
||||||
}
|
}
|
||||||
UiMsg::LogBackTabbed => {
|
UiMsg::LogBackTabbed => {
|
||||||
assert!(self.app.active(&Id::ExplorerHostBridge).is_ok());
|
ui_result(self.app.active(&Id::ExplorerHostBridge));
|
||||||
}
|
}
|
||||||
UiMsg::MarkFile(index) => {
|
UiMsg::MarkFile(index) => {
|
||||||
self.action_mark_file(index);
|
self.action_mark_file(index);
|
||||||
@@ -507,18 +434,16 @@ impl FileTransferActivity {
|
|||||||
self.umount_quit();
|
self.umount_quit();
|
||||||
}
|
}
|
||||||
UiMsg::ShowChmodPopup => {
|
UiMsg::ShowChmodPopup => {
|
||||||
let selected_file = match self.browser.tab() {
|
// On Windows localhost, chmod is not supported
|
||||||
#[cfg(posix)]
|
#[cfg(win)]
|
||||||
FileExplorerTab::HostBridge => self.get_local_selected_entries(),
|
let selected_file = if self.is_local_tab() {
|
||||||
#[cfg(posix)]
|
SelectedFile::None
|
||||||
FileExplorerTab::FindHostBridge => self.get_found_selected_entries(),
|
} else {
|
||||||
FileExplorerTab::Remote => self.get_remote_selected_entries(),
|
self.get_selected_entries()
|
||||||
FileExplorerTab::FindRemote => self.get_found_selected_entries(),
|
|
||||||
#[cfg(win)]
|
|
||||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
|
||||||
SelectedFile::None
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
#[cfg(posix)]
|
||||||
|
let selected_file = self.get_selected_entries();
|
||||||
|
|
||||||
if let Some(mode) = selected_file.unix_pex() {
|
if let Some(mode) = selected_file.unix_pex() {
|
||||||
self.mount_chmod(
|
self.mount_chmod(
|
||||||
mode,
|
mode,
|
||||||
@@ -541,18 +466,8 @@ impl FileTransferActivity {
|
|||||||
self.browser.toggle_terminal(true);
|
self.browser.toggle_terminal(true);
|
||||||
self.mount_exec()
|
self.mount_exec()
|
||||||
}
|
}
|
||||||
UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::HostBridge => {
|
|
||||||
if let SelectedFile::One(file) = self.get_local_selected_entries() {
|
|
||||||
self.mount_file_info(&file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::Remote => {
|
|
||||||
if let SelectedFile::One(file) = self.get_remote_selected_entries() {
|
|
||||||
self.mount_file_info(&file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UiMsg::ShowFileInfoPopup => {
|
UiMsg::ShowFileInfoPopup => {
|
||||||
if let SelectedFile::One(file) = self.get_found_selected_entries() {
|
if let SelectedFile::One(file) = self.get_selected_entries() {
|
||||||
self.mount_file_info(&file);
|
self.mount_file_info(&file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,12 +482,12 @@ impl FileTransferActivity {
|
|||||||
UiMsg::ShowRenamePopup => self.mount_rename(),
|
UiMsg::ShowRenamePopup => self.mount_rename(),
|
||||||
UiMsg::ShowSaveAsPopup => self.mount_saveas(),
|
UiMsg::ShowSaveAsPopup => self.mount_saveas(),
|
||||||
UiMsg::ShowSymlinkPopup => {
|
UiMsg::ShowSymlinkPopup => {
|
||||||
if match self.browser.tab() {
|
// Symlink is not available from find-result tabs
|
||||||
FileExplorerTab::HostBridge => self.is_local_selected_one(),
|
let can_symlink = match self.browser.tab() {
|
||||||
FileExplorerTab::Remote => self.is_remote_selected_one(),
|
FileExplorerTab::HostBridge | FileExplorerTab::Remote => self.is_selected_one(),
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => false,
|
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => false,
|
||||||
} {
|
};
|
||||||
// Only if only one entry is selected
|
if can_symlink {
|
||||||
self.mount_symlink();
|
self.mount_symlink();
|
||||||
} else {
|
} else {
|
||||||
self.mount_error(
|
self.mount_error(
|
||||||
@@ -604,25 +519,25 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
UiMsg::BottomPanelLeft => match self.app.focus() {
|
UiMsg::BottomPanelLeft => match self.app.focus() {
|
||||||
Some(Id::TransferQueueHostBridge) => {
|
Some(Id::TransferQueueHostBridge) => {
|
||||||
assert!(self.app.active(&Id::Log).is_ok())
|
ui_result(self.app.active(&Id::Log));
|
||||||
}
|
}
|
||||||
Some(Id::TransferQueueRemote) => {
|
Some(Id::TransferQueueRemote) => {
|
||||||
assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok())
|
ui_result(self.app.active(&Id::TransferQueueHostBridge));
|
||||||
}
|
}
|
||||||
Some(Id::Log) => {
|
Some(Id::Log) => {
|
||||||
assert!(self.app.active(&Id::TransferQueueRemote).is_ok())
|
ui_result(self.app.active(&Id::TransferQueueRemote));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
UiMsg::BottomPanelRight => match self.app.focus() {
|
UiMsg::BottomPanelRight => match self.app.focus() {
|
||||||
Some(Id::TransferQueueHostBridge) => {
|
Some(Id::TransferQueueHostBridge) => {
|
||||||
assert!(self.app.active(&Id::TransferQueueRemote).is_ok())
|
ui_result(self.app.active(&Id::TransferQueueRemote));
|
||||||
}
|
}
|
||||||
Some(Id::TransferQueueRemote) => {
|
Some(Id::TransferQueueRemote) => {
|
||||||
assert!(self.app.active(&Id::Log).is_ok())
|
ui_result(self.app.active(&Id::Log));
|
||||||
}
|
}
|
||||||
Some(Id::Log) => {
|
Some(Id::Log) => {
|
||||||
assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok())
|
ui_result(self.app.active(&Id::TransferQueueHostBridge));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,388 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
|
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
|
||||||
|
use tuirealm::ratatui::widgets::Clear;
|
||||||
|
use tuirealm::{Attribute, Sub, SubClause, SubEventClause};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::browser::FoundExplorerTab;
|
||||||
|
use crate::ui::activities::filetransfer::{
|
||||||
|
Context, FileTransferActivity, Id, components, ui_result,
|
||||||
|
};
|
||||||
|
use crate::utils::ui::{Popup, Size};
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
// -- init
|
||||||
|
|
||||||
|
/// Initialize file transfer activity's view
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn init(&mut self) {
|
||||||
|
// Mount local file explorer
|
||||||
|
let local_explorer_background = self.theme().transfer_local_explorer_background;
|
||||||
|
let local_explorer_foreground = self.theme().transfer_local_explorer_foreground;
|
||||||
|
let local_explorer_highlighted = self.theme().transfer_local_explorer_highlighted;
|
||||||
|
let remote_explorer_background = self.theme().transfer_remote_explorer_background;
|
||||||
|
let remote_explorer_foreground = self.theme().transfer_remote_explorer_foreground;
|
||||||
|
let remote_explorer_highlighted = self.theme().transfer_remote_explorer_highlighted;
|
||||||
|
let key_color = self.theme().misc_keys;
|
||||||
|
let log_panel = self.theme().transfer_log_window;
|
||||||
|
let log_background = self.theme().transfer_log_background;
|
||||||
|
ui_result(self.app.mount(
|
||||||
|
Id::FooterBar,
|
||||||
|
Box::new(components::FooterBar::new(key_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.mount(
|
||||||
|
Id::ExplorerHostBridge,
|
||||||
|
Box::new(components::ExplorerLocal::new(
|
||||||
|
"",
|
||||||
|
&[],
|
||||||
|
local_explorer_background,
|
||||||
|
local_explorer_foreground,
|
||||||
|
local_explorer_highlighted,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.mount(
|
||||||
|
Id::ExplorerRemote,
|
||||||
|
Box::new(components::ExplorerRemote::new(
|
||||||
|
"",
|
||||||
|
&[],
|
||||||
|
remote_explorer_background,
|
||||||
|
remote_explorer_foreground,
|
||||||
|
remote_explorer_highlighted,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.mount(
|
||||||
|
Id::Log,
|
||||||
|
Box::new(components::Log::new(vec![], log_panel, log_background)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
self.refresh_host_bridge_transfer_queue();
|
||||||
|
self.refresh_remote_transfer_queue();
|
||||||
|
// Load status bar
|
||||||
|
self.refresh_local_status_bar();
|
||||||
|
self.refresh_remote_status_bar();
|
||||||
|
// Update components
|
||||||
|
self.update_host_bridge_filelist();
|
||||||
|
// self.update_remote_filelist();
|
||||||
|
// Global listener
|
||||||
|
self.mount_global_listener();
|
||||||
|
// Give focus to local explorer
|
||||||
|
ui_result(self.app.active(&Id::ExplorerHostBridge));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- view
|
||||||
|
|
||||||
|
/// View gui
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn view(&mut self) {
|
||||||
|
self.redraw = false;
|
||||||
|
let mut context: Context = self.context.take().unwrap();
|
||||||
|
let _ = context.terminal.raw_mut().draw(|f| {
|
||||||
|
// Prepare chunks
|
||||||
|
let body = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Min(7), // Body
|
||||||
|
Constraint::Length(1), // Footer
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(f.area());
|
||||||
|
// main chunks
|
||||||
|
let main_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Percentage(70), // Explorer
|
||||||
|
Constraint::Percentage(30), // Log
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(body[0]);
|
||||||
|
// Create explorer chunks
|
||||||
|
let tabs_chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.split(main_chunks[0]);
|
||||||
|
// Create log box chunks
|
||||||
|
let bottom_chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Length(1), Constraint::Length(10)].as_ref())
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.split(main_chunks[1]);
|
||||||
|
// Create status bar chunks
|
||||||
|
let status_bar_chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.horizontal_margin(1)
|
||||||
|
.split(bottom_chunks[0]);
|
||||||
|
let bottom_components = Layout::default()
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Percentage(25),
|
||||||
|
Constraint::Percentage(25),
|
||||||
|
Constraint::Percentage(50),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.split(bottom_chunks[1]);
|
||||||
|
// Draw footer
|
||||||
|
self.app.view(&Id::FooterBar, f, body[1]);
|
||||||
|
// Draw explorers
|
||||||
|
// @! Local explorer (Find or default)
|
||||||
|
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) {
|
||||||
|
self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]);
|
||||||
|
} else if self.browser.is_terminal_open_host_bridge() {
|
||||||
|
self.app.view(&Id::TerminalHostBridge, f, tabs_chunks[0]);
|
||||||
|
} else {
|
||||||
|
self.app.view(&Id::ExplorerHostBridge, f, tabs_chunks[0]);
|
||||||
|
}
|
||||||
|
// @! Remote explorer (Find or default)
|
||||||
|
if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) {
|
||||||
|
self.app.view(&Id::ExplorerFind, f, tabs_chunks[1]);
|
||||||
|
} else if self.browser.is_terminal_open_remote() {
|
||||||
|
self.app.view(&Id::TerminalRemote, f, tabs_chunks[1]);
|
||||||
|
} else {
|
||||||
|
self.app.view(&Id::ExplorerRemote, f, tabs_chunks[1]);
|
||||||
|
}
|
||||||
|
// draw transfer queues
|
||||||
|
self.app
|
||||||
|
.view(&Id::TransferQueueHostBridge, f, bottom_components[0]);
|
||||||
|
self.app
|
||||||
|
.view(&Id::TransferQueueRemote, f, bottom_components[1]);
|
||||||
|
// Draw log box
|
||||||
|
self.app.view(&Id::Log, f, bottom_components[2]);
|
||||||
|
// Draw status bar
|
||||||
|
self.app
|
||||||
|
.view(&Id::StatusBarHostBridge, f, status_bar_chunks[0]);
|
||||||
|
self.app.view(&Id::StatusBarRemote, f, status_bar_chunks[1]);
|
||||||
|
// @! Draw popups — first mounted popup in priority order wins
|
||||||
|
let popup_priority = [
|
||||||
|
Id::FatalPopup,
|
||||||
|
Id::CopyPopup,
|
||||||
|
Id::ChmodPopup,
|
||||||
|
Id::FilterPopup,
|
||||||
|
Id::GotoPopup,
|
||||||
|
Id::MkdirPopup,
|
||||||
|
Id::NewfilePopup,
|
||||||
|
Id::OpenWithPopup,
|
||||||
|
Id::RenamePopup,
|
||||||
|
Id::SaveAsPopup,
|
||||||
|
Id::SymlinkPopup,
|
||||||
|
Id::FileInfoPopup,
|
||||||
|
Id::ProgressBarPartial,
|
||||||
|
Id::DeletePopup,
|
||||||
|
Id::ReplacePopup,
|
||||||
|
Id::DisconnectPopup,
|
||||||
|
Id::QuitPopup,
|
||||||
|
Id::WatchedPathsList,
|
||||||
|
Id::WatcherPopup,
|
||||||
|
Id::SortingPopup,
|
||||||
|
Id::ErrorPopup,
|
||||||
|
Id::WaitPopup,
|
||||||
|
Id::SyncBrowsingMkdirPopup,
|
||||||
|
Id::KeybindingsPopup,
|
||||||
|
];
|
||||||
|
if let Some(popup_id) = popup_priority.iter().find(|id| self.app.mounted(id)) {
|
||||||
|
match popup_id {
|
||||||
|
// Dynamic-height popups (text wrapping)
|
||||||
|
Id::FatalPopup | Id::ErrorPopup => {
|
||||||
|
let popup = Popup(
|
||||||
|
Size::Percentage(50),
|
||||||
|
self.calc_popup_height(
|
||||||
|
popup_id.clone(),
|
||||||
|
f.area().width,
|
||||||
|
f.area().height,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.draw_in(f.area());
|
||||||
|
f.render_widget(Clear, popup);
|
||||||
|
self.app.view(popup_id, f, popup);
|
||||||
|
}
|
||||||
|
// Dual-component progress bar
|
||||||
|
Id::ProgressBarPartial => {
|
||||||
|
let popup =
|
||||||
|
Popup(Size::Percentage(50), Size::Percentage(20)).draw_in(f.area());
|
||||||
|
f.render_widget(Clear, popup);
|
||||||
|
let popup_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[Constraint::Percentage(50), Constraint::Percentage(50)].as_ref(),
|
||||||
|
)
|
||||||
|
.split(popup);
|
||||||
|
self.app.view(&Id::ProgressBarFull, f, popup_chunks[0]);
|
||||||
|
self.app.view(&Id::ProgressBarPartial, f, popup_chunks[1]);
|
||||||
|
}
|
||||||
|
// Wait popup with dynamic line count
|
||||||
|
Id::WaitPopup => {
|
||||||
|
let lines = self
|
||||||
|
.app
|
||||||
|
.query(&Id::WaitPopup, Attribute::Text)
|
||||||
|
.map(|x| x.map(|x| x.unwrap_payload().unwrap_vec().len()))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.unwrap_or(1) as u16;
|
||||||
|
let popup =
|
||||||
|
Popup(Size::Percentage(50), Size::Unit(2 + lines)).draw_in(f.area());
|
||||||
|
f.render_widget(Clear, popup);
|
||||||
|
self.app.view(&Id::WaitPopup, f, popup);
|
||||||
|
}
|
||||||
|
// Standard fixed-size popups
|
||||||
|
id => {
|
||||||
|
let (w, h) = Self::popup_dimensions(id);
|
||||||
|
let popup = Popup(w, h).draw_in(f.area());
|
||||||
|
f.render_widget(Clear, popup);
|
||||||
|
self.app.view(id, f, popup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Re-give context
|
||||||
|
self.context = Some(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- popup dimensions
|
||||||
|
|
||||||
|
/// Returns the fixed (width, height) for a standard popup.
|
||||||
|
fn popup_dimensions(id: &Id) -> (Size, Size) {
|
||||||
|
match id {
|
||||||
|
Id::CopyPopup
|
||||||
|
| Id::GotoPopup
|
||||||
|
| Id::MkdirPopup
|
||||||
|
| Id::NewfilePopup
|
||||||
|
| Id::OpenWithPopup
|
||||||
|
| Id::RenamePopup
|
||||||
|
| Id::SaveAsPopup => (Size::Percentage(40), Size::Unit(3)),
|
||||||
|
Id::ChmodPopup => (Size::Percentage(50), Size::Unit(12)),
|
||||||
|
Id::FilterPopup | Id::SymlinkPopup | Id::SortingPopup | Id::ReplacePopup => {
|
||||||
|
(Size::Percentage(50), Size::Unit(3))
|
||||||
|
}
|
||||||
|
Id::FileInfoPopup => (Size::Percentage(80), Size::Percentage(50)),
|
||||||
|
Id::DeletePopup | Id::DisconnectPopup | Id::QuitPopup => {
|
||||||
|
(Size::Percentage(30), Size::Unit(3))
|
||||||
|
}
|
||||||
|
Id::WatchedPathsList => (Size::Percentage(60), Size::Percentage(50)),
|
||||||
|
Id::WatcherPopup | Id::SyncBrowsingMkdirPopup => (Size::Percentage(60), Size::Unit(3)),
|
||||||
|
Id::KeybindingsPopup => (Size::Percentage(50), Size::Percentage(80)),
|
||||||
|
_ => (Size::Percentage(50), Size::Unit(3)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given the id of the component to display and the width and height of the total area,
|
||||||
|
/// returns the height in percentage to the entire area height, that the popup should have
|
||||||
|
fn calc_popup_height(&self, id: Id, width: u16, height: u16) -> Size {
|
||||||
|
// Get current text width
|
||||||
|
let text_width = self
|
||||||
|
.app
|
||||||
|
.query(&id, tuirealm::Attribute::Text)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|x| {
|
||||||
|
if x.as_payload().is_none() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
x.unwrap_payload()
|
||||||
|
.unwrap_vec()
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| x.unwrap_text_span().content)
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("")
|
||||||
|
.width() as u16
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
// Calc real width of a row in the popup
|
||||||
|
let row_width = (width / 2).saturating_sub(2);
|
||||||
|
// Calc row height in percentage (1 : height = x : 100)
|
||||||
|
let row_height_p = (100.0 / (height as f64)).ceil() as u16;
|
||||||
|
// Get amount of required rows NOTE: + 2 because of margins
|
||||||
|
let display_rows = ((text_width as f64) / (row_width as f64)).ceil() as u16 + 2;
|
||||||
|
// Return height (row_height_p * display_rows)
|
||||||
|
Size::Percentage(display_rows * row_height_p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- global listener
|
||||||
|
|
||||||
|
fn mount_global_listener(&mut self) {
|
||||||
|
ui_result(self.app.mount(
|
||||||
|
Id::GlobalListener,
|
||||||
|
Box::<components::GlobalListener>::default(),
|
||||||
|
vec![
|
||||||
|
Sub::new(
|
||||||
|
SubEventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Esc,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(
|
||||||
|
SubEventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('h'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(
|
||||||
|
SubEventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Function(1),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(
|
||||||
|
SubEventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Function(10),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(
|
||||||
|
SubEventClause::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('q'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}),
|
||||||
|
Self::no_popup_mounted_clause(),
|
||||||
|
),
|
||||||
|
Sub::new(SubEventClause::WindowResize, SubClause::Always),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a sub clause which requires that no popup is mounted in order to be satisfied
|
||||||
|
fn no_popup_mounted_clause() -> SubClause<Id> {
|
||||||
|
tuirealm::subclause_and_not!(
|
||||||
|
Id::CopyPopup,
|
||||||
|
Id::DeletePopup,
|
||||||
|
Id::DisconnectPopup,
|
||||||
|
Id::ErrorPopup,
|
||||||
|
Id::TerminalHostBridge,
|
||||||
|
Id::TerminalRemote,
|
||||||
|
Id::FatalPopup,
|
||||||
|
Id::FileInfoPopup,
|
||||||
|
Id::GotoPopup,
|
||||||
|
Id::KeybindingsPopup,
|
||||||
|
Id::MkdirPopup,
|
||||||
|
Id::NewfilePopup,
|
||||||
|
Id::OpenWithPopup,
|
||||||
|
Id::ProgressBarFull,
|
||||||
|
Id::ProgressBarPartial,
|
||||||
|
Id::ExplorerFind,
|
||||||
|
Id::QuitPopup,
|
||||||
|
Id::RenamePopup,
|
||||||
|
Id::ReplacePopup,
|
||||||
|
Id::SaveAsPopup,
|
||||||
|
Id::SortingPopup,
|
||||||
|
Id::SyncBrowsingMkdirPopup,
|
||||||
|
Id::SymlinkPopup,
|
||||||
|
Id::WatcherPopup,
|
||||||
|
Id::WatchedPathsList,
|
||||||
|
Id::ChmodPopup,
|
||||||
|
Id::WaitPopup,
|
||||||
|
Id::FilterPopup
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,602 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
use remotefs::fs::{File, UnixPex};
|
||||||
|
use tuirealm::props::{PropPayload, PropValue, TextSpan};
|
||||||
|
use tuirealm::{AttrValue, Attribute};
|
||||||
|
|
||||||
|
use crate::explorer::FileSorting;
|
||||||
|
use crate::ui::activities::filetransfer::browser::FileExplorerTab;
|
||||||
|
use crate::ui::activities::filetransfer::components::ATTR_FILES;
|
||||||
|
use crate::ui::activities::filetransfer::{FileTransferActivity, Id, components, ui_result};
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
// -- partials
|
||||||
|
|
||||||
|
/// Mount info box
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_info<S: AsRef<str>>(&mut self, text: S) {
|
||||||
|
// Mount
|
||||||
|
let info_color = self.theme().misc_info_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::ErrorPopup,
|
||||||
|
Box::new(components::ErrorPopup::new(text, info_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::ErrorPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mount error box
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_error<S: AsRef<str>>(&mut self, text: S) {
|
||||||
|
// Mount
|
||||||
|
let error_color = self.theme().misc_error_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::ErrorPopup,
|
||||||
|
Box::new(components::ErrorPopup::new(text, error_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::ErrorPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Umount error message
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_error(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::ErrorPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_fatal<S: AsRef<str>>(&mut self, text: S) {
|
||||||
|
self.umount_wait();
|
||||||
|
// Mount
|
||||||
|
let error_color = self.theme().misc_error_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::FatalPopup,
|
||||||
|
Box::new(components::FatalPopup::new(text, error_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::FatalPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Umount fatal error message
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_fatal(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::FatalPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_wait<S: AsRef<str>>(&mut self, text: S) {
|
||||||
|
let color = self.theme().misc_info_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::WaitPopup,
|
||||||
|
Box::new(components::WaitPopup::new(text, color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::WaitPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_walkdir_wait(&mut self) {
|
||||||
|
let color = self.theme().misc_info_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::WaitPopup,
|
||||||
|
Box::new(components::WalkdirWaitPopup::new(
|
||||||
|
"Scanning current directory…",
|
||||||
|
color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::WaitPopup));
|
||||||
|
|
||||||
|
self.view();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn update_walkdir_entries(
|
||||||
|
&mut self,
|
||||||
|
entries: usize,
|
||||||
|
) {
|
||||||
|
let text = format!("Scanning current directory… ({entries} items found)",);
|
||||||
|
let _ = self.app.attr(
|
||||||
|
&Id::WaitPopup,
|
||||||
|
Attribute::Text,
|
||||||
|
AttrValue::Payload(PropPayload::Vec(vec![
|
||||||
|
PropValue::TextSpan(TextSpan::from(text)),
|
||||||
|
PropValue::TextSpan(TextSpan::from("Press 'CTRL+C' to abort")),
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.view();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_blocking_wait<S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
text: S,
|
||||||
|
) {
|
||||||
|
self.mount_wait(text);
|
||||||
|
self.view();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_wait(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::WaitPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mount quit popup
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_quit(&mut self) {
|
||||||
|
// Protocol
|
||||||
|
let quit_color = self.theme().misc_quit_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::QuitPopup,
|
||||||
|
Box::new(components::QuitPopup::new(quit_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::QuitPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Umount quit popup
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_quit(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::QuitPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mount disconnect popup
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_disconnect(&mut self) {
|
||||||
|
// Protocol
|
||||||
|
let quit_color = self.theme().misc_quit_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::DisconnectPopup,
|
||||||
|
Box::new(components::DisconnectPopup::new(quit_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::DisconnectPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Umount disconnect popup
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_disconnect(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::DisconnectPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_chmod(
|
||||||
|
&mut self,
|
||||||
|
mode: UnixPex,
|
||||||
|
title: String,
|
||||||
|
) {
|
||||||
|
// Mount
|
||||||
|
let color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::ChmodPopup,
|
||||||
|
Box::new(components::ChmodPopup::new(mode, color, title)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::ChmodPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_chmod(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::ChmodPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_filter(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::FilterPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_filter(&mut self) {
|
||||||
|
let input_color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::FilterPopup,
|
||||||
|
Box::new(components::FilterPopup::new(input_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::FilterPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_copy(&mut self) {
|
||||||
|
let input_color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::CopyPopup,
|
||||||
|
Box::new(components::CopyPopup::new(input_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::CopyPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_copy(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::CopyPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_exec(&mut self) {
|
||||||
|
let tab = self.browser.tab();
|
||||||
|
let id = match tab {
|
||||||
|
FileExplorerTab::HostBridge => Id::TerminalHostBridge,
|
||||||
|
FileExplorerTab::Remote => Id::TerminalRemote,
|
||||||
|
_ => {
|
||||||
|
error!("Cannot mount terminal on this tab");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let border = match tab {
|
||||||
|
FileExplorerTab::HostBridge => self.theme().transfer_local_explorer_highlighted,
|
||||||
|
FileExplorerTab::Remote => self.theme().transfer_remote_explorer_highlighted,
|
||||||
|
_ => {
|
||||||
|
error!("Cannot mount terminal on this tab");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let input_color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(
|
||||||
|
self.app.remount(
|
||||||
|
id.clone(),
|
||||||
|
Box::new(
|
||||||
|
components::Terminal::default()
|
||||||
|
.foreground(input_color)
|
||||||
|
.prompt(self.terminal_prompt())
|
||||||
|
.title(format!("Terminal - {}", self.get_tab_hostname()))
|
||||||
|
.border_color(border),
|
||||||
|
),
|
||||||
|
vec![],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ui_result(self.app.active(&id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the terminal prompt based on the current directory
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn update_terminal_prompt(&mut self) {
|
||||||
|
let prompt = self.terminal_prompt();
|
||||||
|
let id = match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge => Id::TerminalHostBridge,
|
||||||
|
FileExplorerTab::Remote => Id::TerminalRemote,
|
||||||
|
_ => {
|
||||||
|
error!("Cannot update terminal prompt on this tab");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = self
|
||||||
|
.app
|
||||||
|
.attr(&id, Attribute::Content, AttrValue::String(prompt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print output to terminal
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn print_terminal(&mut self, text: String) {
|
||||||
|
// get id
|
||||||
|
let focus = self.app.focus().unwrap().clone();
|
||||||
|
|
||||||
|
// replace all \n with \r\n
|
||||||
|
let mut text = text.replace('\n', "\r\n");
|
||||||
|
if !text.ends_with("\r\n") && !text.is_empty() {
|
||||||
|
text.push_str("\r\n");
|
||||||
|
}
|
||||||
|
let _ = self
|
||||||
|
.app
|
||||||
|
.attr(&focus, Attribute::Text, AttrValue::String(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_exec(&mut self) {
|
||||||
|
let focus = self.app.focus().unwrap().clone();
|
||||||
|
let _ = self.app.umount(&focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_find(
|
||||||
|
&mut self,
|
||||||
|
msg: impl ToString,
|
||||||
|
fuzzy_search: bool,
|
||||||
|
) {
|
||||||
|
// Get color
|
||||||
|
let (bg, fg, hg) = match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => (
|
||||||
|
self.theme().transfer_local_explorer_background,
|
||||||
|
self.theme().transfer_local_explorer_foreground,
|
||||||
|
self.theme().transfer_local_explorer_highlighted,
|
||||||
|
),
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => (
|
||||||
|
self.theme().transfer_remote_explorer_background,
|
||||||
|
self.theme().transfer_remote_explorer_foreground,
|
||||||
|
self.theme().transfer_remote_explorer_highlighted,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mount component
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::ExplorerFind,
|
||||||
|
if fuzzy_search {
|
||||||
|
Box::new(components::ExplorerFuzzy::new(
|
||||||
|
msg.to_string(),
|
||||||
|
&[],
|
||||||
|
bg,
|
||||||
|
fg,
|
||||||
|
hg,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Box::new(components::ExplorerFind::new(
|
||||||
|
msg.to_string(),
|
||||||
|
&[],
|
||||||
|
bg,
|
||||||
|
fg,
|
||||||
|
hg,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::ExplorerFind));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_find(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::ExplorerFind);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_goto(&mut self) {
|
||||||
|
// get files
|
||||||
|
let files = self
|
||||||
|
.browser
|
||||||
|
.explorer()
|
||||||
|
.iter_files()
|
||||||
|
.filter(|f| f.is_dir() || f.is_symlink())
|
||||||
|
.map(|f| f.path().to_string_lossy().to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
let input_color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::GotoPopup,
|
||||||
|
Box::new(components::GotoPopup::new(input_color, files)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::GotoPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn update_goto(&mut self, files: Vec<String>) {
|
||||||
|
let payload = files
|
||||||
|
.into_iter()
|
||||||
|
.map(PropValue::Str)
|
||||||
|
.collect::<Vec<PropValue>>();
|
||||||
|
|
||||||
|
let _ = self.app.attr(
|
||||||
|
&Id::GotoPopup,
|
||||||
|
Attribute::Custom(ATTR_FILES),
|
||||||
|
AttrValue::Payload(PropPayload::Vec(payload)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_goto(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::GotoPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_mkdir(&mut self) {
|
||||||
|
let input_color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::MkdirPopup,
|
||||||
|
Box::new(components::MkdirPopup::new(input_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::MkdirPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_mkdir(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::MkdirPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_newfile(&mut self) {
|
||||||
|
let input_color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::NewfilePopup,
|
||||||
|
Box::new(components::NewfilePopup::new(input_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::NewfilePopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_newfile(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::NewfilePopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_openwith(&mut self) {
|
||||||
|
let input_color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::OpenWithPopup,
|
||||||
|
Box::new(components::OpenWithPopup::new(input_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::OpenWithPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_openwith(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::OpenWithPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_rename(&mut self) {
|
||||||
|
let input_color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::RenamePopup,
|
||||||
|
Box::new(components::RenamePopup::new(input_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::RenamePopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_rename(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::RenamePopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_saveas(&mut self) {
|
||||||
|
let input_color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::SaveAsPopup,
|
||||||
|
Box::new(components::SaveAsPopup::new(input_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::SaveAsPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_saveas(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::SaveAsPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_progress_bar(
|
||||||
|
&mut self,
|
||||||
|
root_name: String,
|
||||||
|
) {
|
||||||
|
let prog_color_full = self.theme().transfer_progress_bar_full;
|
||||||
|
let prog_color_partial = self.theme().transfer_progress_bar_partial;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::ProgressBarFull,
|
||||||
|
Box::new(components::ProgressBarFull::new(
|
||||||
|
0.0,
|
||||||
|
"",
|
||||||
|
&root_name,
|
||||||
|
prog_color_full,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::ProgressBarPartial,
|
||||||
|
Box::new(components::ProgressBarPartial::new(
|
||||||
|
0.0,
|
||||||
|
"",
|
||||||
|
"Please wait",
|
||||||
|
prog_color_partial,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::ProgressBarPartial));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_progress_bar(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::ProgressBarPartial);
|
||||||
|
let _ = self.app.umount(&Id::ProgressBarFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_file_sorting(&mut self) {
|
||||||
|
let sorting_color = self.theme().transfer_status_sorting;
|
||||||
|
let sorting: FileSorting = match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge => self.host_bridge().get_file_sorting(),
|
||||||
|
FileExplorerTab::Remote => self.remote().get_file_sorting(),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::SortingPopup,
|
||||||
|
Box::new(components::SortingPopup::new(sorting, sorting_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::SortingPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_file_sorting(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::SortingPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_radio_delete(&mut self) {
|
||||||
|
let warn_color = self.theme().misc_warn_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::DeletePopup,
|
||||||
|
Box::new(components::DeletePopup::new(warn_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::DeletePopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_radio_delete(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::DeletePopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_radio_watch(
|
||||||
|
&mut self,
|
||||||
|
watch: bool,
|
||||||
|
local: &str,
|
||||||
|
remote: &str,
|
||||||
|
) {
|
||||||
|
let info_color = self.theme().misc_info_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::WatcherPopup,
|
||||||
|
Box::new(components::WatcherPopup::new(
|
||||||
|
watch, local, remote, info_color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::WatcherPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_radio_watcher(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::WatcherPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_watched_paths_list(
|
||||||
|
&mut self,
|
||||||
|
paths: &[std::path::PathBuf],
|
||||||
|
) {
|
||||||
|
let info_color = self.theme().misc_info_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::WatchedPathsList,
|
||||||
|
Box::new(components::WatchedPathsList::new(paths, info_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::WatchedPathsList));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_watched_paths_list(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::WatchedPathsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_radio_replace(&mut self, file_name: &str) {
|
||||||
|
let warn_color = self.theme().misc_warn_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::ReplacePopup,
|
||||||
|
Box::new(components::ReplacePopup::new(Some(file_name), warn_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::ReplacePopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_radio_replace(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::ReplacePopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_file_info(&mut self, file: &File) {
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::FileInfoPopup,
|
||||||
|
Box::new(components::FileInfoPopup::new(file)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::FileInfoPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_file_info(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::FileInfoPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_symlink(&mut self) {
|
||||||
|
let input_color = self.theme().misc_input_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::SymlinkPopup,
|
||||||
|
Box::new(components::SymlinkPopup::new(input_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::SymlinkPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_symlink(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::SymlinkPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_sync_browsing_mkdir_popup(
|
||||||
|
&mut self,
|
||||||
|
dir_name: &str,
|
||||||
|
) {
|
||||||
|
let color = self.theme().misc_info_dialog;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::SyncBrowsingMkdirPopup,
|
||||||
|
Box::new(components::SyncBrowsingMkdirPopup::new(color, dir_name)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::SyncBrowsingMkdirPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_sync_browsing_mkdir_popup(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::SyncBrowsingMkdirPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mount help
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn mount_help(&mut self) {
|
||||||
|
let key_color = self.theme().misc_keys;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::KeybindingsPopup,
|
||||||
|
Box::new(components::KeybindingsPopup::new(key_color)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
ui_result(self.app.active(&Id::KeybindingsPopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn umount_help(&mut self) {
|
||||||
|
let _ = self.app.umount(&Id::KeybindingsPopup);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
use crate::ui::activities::filetransfer::{
|
||||||
|
FileTransferActivity, Id, MarkQueue, components, ui_result,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn refresh_host_bridge_transfer_queue(&mut self) {
|
||||||
|
let enqueued = self
|
||||||
|
.host_bridge()
|
||||||
|
.enqueued()
|
||||||
|
.iter()
|
||||||
|
.map(|(src, dest)| (src.clone(), dest.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let log_panel = self.theme().transfer_log_window;
|
||||||
|
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::TransferQueueHostBridge,
|
||||||
|
Box::new(components::SelectedFilesList::new(
|
||||||
|
&enqueued,
|
||||||
|
MarkQueue::Local,
|
||||||
|
log_panel,
|
||||||
|
"Host Bridge selected files",
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn refresh_remote_transfer_queue(&mut self) {
|
||||||
|
let enqueued = self
|
||||||
|
.remote()
|
||||||
|
.enqueued()
|
||||||
|
.iter()
|
||||||
|
.map(|(src, dest)| (src.clone(), dest.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let log_panel = self.theme().transfer_log_window;
|
||||||
|
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::TransferQueueRemote,
|
||||||
|
Box::new(components::SelectedFilesList::new(
|
||||||
|
&enqueued,
|
||||||
|
MarkQueue::Remote,
|
||||||
|
log_panel,
|
||||||
|
"Remote transfer selected files",
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn refresh_local_status_bar(&mut self) {
|
||||||
|
let sorting_color = self.theme().transfer_status_sorting;
|
||||||
|
let hidden_color = self.theme().transfer_status_hidden;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::StatusBarHostBridge,
|
||||||
|
Box::new(components::StatusBarLocal::new(
|
||||||
|
&self.browser,
|
||||||
|
sorting_color,
|
||||||
|
hidden_color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui::activities::filetransfer) fn refresh_remote_status_bar(&mut self) {
|
||||||
|
let sorting_color = self.theme().transfer_status_sorting;
|
||||||
|
let hidden_color = self.theme().transfer_status_hidden;
|
||||||
|
let sync_color = self.theme().transfer_status_sync_browsing;
|
||||||
|
ui_result(self.app.remount(
|
||||||
|
Id::StatusBarRemote,
|
||||||
|
Box::new(components::StatusBarRemote::new(
|
||||||
|
&self.browser,
|
||||||
|
sorting_color,
|
||||||
|
hidden_color,
|
||||||
|
sync_color,
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -338,9 +338,9 @@ fn parse_kube_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
|||||||
fn parse_smb_remote_opts(s: &str) -> Result<FileTransferParams, String> {
|
fn parse_smb_remote_opts(s: &str) -> Result<FileTransferParams, String> {
|
||||||
match REMOTE_SMB_OPT_REGEX.captures(s) {
|
match REMOTE_SMB_OPT_REGEX.captures(s) {
|
||||||
Some(groups) => {
|
Some(groups) => {
|
||||||
let username: Option<String> = match groups.get(1) {
|
let username = match groups.get(1) {
|
||||||
Some(group) => Some(group.as_str().to_string()),
|
Some(group) => Some(group.as_str().to_string()),
|
||||||
None => Some(whoami::username()),
|
None => whoami::username().ok(),
|
||||||
};
|
};
|
||||||
let address = match groups.get(2) {
|
let address = match groups.get(2) {
|
||||||
Some(group) => group.as_str().to_string(),
|
Some(group) => group.as_str().to_string(),
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
// Ext
|
// Ext
|
||||||
|
|
||||||
use rand::distr::Alphanumeric;
|
use rand::distr::Alphanumeric;
|
||||||
use rand::{Rng, rng};
|
use rand::{RngExt as _, rng};
|
||||||
|
|
||||||
/// Generate a random alphanumeric string with provided length
|
/// Generate a random alphanumeric string with provided length
|
||||||
pub fn random_alphanumeric_with_len(len: usize) -> String {
|
pub fn random_alphanumeric_with_len(len: usize) -> String {
|
||||||
|
|||||||
Reference in New Issue
Block a user