mirror of
https://github.com/veeso/termscp.git
synced 2026-04-01 07:42:17 -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:
committed by
GitHub
parent
3fb61a76fe
commit
a252caa66b
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,3 +24,7 @@ dist/pkgs/arch/*.tar.gz
|
||||
|
||||
dist/pkgs/
|
||||
dist/build/macos/openssl/
|
||||
|
||||
.idea/
|
||||
.claude/
|
||||
|
||||
|
||||
109
CLAUDE.md
Normal file
109
CLAUDE.md
Normal file
@@ -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/`
|
||||
1203
Cargo.lock
generated
1203
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -55,7 +55,7 @@ notify = "8"
|
||||
notify-rust = { version = "^4", default-features = false, features = ["d"] }
|
||||
nucleo = "0.5"
|
||||
open = "5"
|
||||
rand = "^0.9"
|
||||
rand = "0.10"
|
||||
regex = "^1"
|
||||
remotefs = "^0.3"
|
||||
remotefs-aws-s3 = "0.4"
|
||||
@@ -73,21 +73,21 @@ self_update = { version = "^0.42", default-features = false, features = [
|
||||
serde = { version = "^1", features = ["derive"] }
|
||||
shellexpand = "3"
|
||||
simplelog = "^0.12"
|
||||
ssh2-config = "^0.6"
|
||||
ssh2-config = "^0.7"
|
||||
tempfile = "3"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
toml = "^0.9"
|
||||
toml = "1"
|
||||
tui-realm-stdlib = "3"
|
||||
tuirealm = "3"
|
||||
tui-term = "0.2"
|
||||
unicode-width = "0.2"
|
||||
version-compare = "^0.2"
|
||||
whoami = "^1.6"
|
||||
whoami = "2"
|
||||
wildmatch = "2"
|
||||
|
||||
[target."cfg(target_family = \"unix\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.3", features = [
|
||||
remotefs-ftp = { version = "^0.4", features = [
|
||||
"native-tls-vendored",
|
||||
"native-tls",
|
||||
] }
|
||||
@@ -98,7 +98,7 @@ remotefs-ssh = { version = "^0.7", default-features = false, features = [
|
||||
uzers = "0.12"
|
||||
|
||||
[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" }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -107,7 +107,7 @@ serial_test = "^3"
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2"
|
||||
vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] }
|
||||
vergen-git2 = { version = "9", features = ["build", "cargo", "rustc", "si"] }
|
||||
|
||||
[features]
|
||||
default = ["smb", "keyring"]
|
||||
|
||||
48
cliff.toml
Normal file
48
cliff.toml
Normal file
@@ -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
|
||||
let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config());
|
||||
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
|
||||
.resolve(
|
||||
&generic_params.address,
|
||||
&generic_params
|
||||
.username
|
||||
.clone()
|
||||
.unwrap_or(whoami::username()),
|
||||
)
|
||||
.resolve(&generic_params.address, &username)
|
||||
.is_none()
|
||||
{
|
||||
debug!(
|
||||
|
||||
@@ -35,7 +35,7 @@ impl HostBridgeParams {
|
||||
/// Returns the host name for the bridge params
|
||||
pub fn username(&self) -> Option<String> {
|
||||
match self {
|
||||
HostBridgeParams::Localhost(_) => Some(whoami::username()),
|
||||
HostBridgeParams::Localhost(_) => whoami::username().ok(),
|
||||
HostBridgeParams::Remote(_, params) => {
|
||||
params.generic_params().and_then(|p| p.username.clone())
|
||||
}
|
||||
|
||||
@@ -173,18 +173,16 @@ impl RemoteFsBuilder {
|
||||
credentials = credentials.workgroup(workgroup);
|
||||
}
|
||||
|
||||
match SmbFs::try_new(
|
||||
SmbFs::try_new(
|
||||
credentials,
|
||||
SmbOptions::default()
|
||||
.one_share_per_server(true)
|
||||
.case_sensitive(false),
|
||||
) {
|
||||
Ok(fs) => fs,
|
||||
Err(e) => {
|
||||
error!("Invalid params for protocol SMB: {e}");
|
||||
panic!("Invalid params for protocol SMB: {e}")
|
||||
}
|
||||
}
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Invalid params for protocol SMB: {e}");
|
||||
panic!("Invalid params for protocol SMB: {e}")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(smb_windows)]
|
||||
@@ -236,12 +234,13 @@ impl RemoteFsBuilder {
|
||||
} else {
|
||||
//* case 3: use system username; can't be None
|
||||
debug!("no username was provided, using current username");
|
||||
opts = opts.username(whoami::username());
|
||||
if let Ok(username) = whoami::username() {
|
||||
opts = opts.username(username);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//* case 3: use system username; can't be None
|
||||
} else if let Ok(username) = whoami::username() {
|
||||
debug!("no username was provided, using current username");
|
||||
opts = opts.username(whoami::username());
|
||||
opts = opts.username(username);
|
||||
}
|
||||
// For SSH protocols, only set password if explicitly provided and non-empty.
|
||||
// This allows the SSH library to prioritize key-based and agent authentication.
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//!
|
||||
//! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system
|
||||
|
||||
// Crate
|
||||
// Ext
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::string::ToString;
|
||||
@@ -12,11 +10,8 @@ use std::time::SystemTime;
|
||||
use super::keys::filestorage::FileStorage;
|
||||
use super::keys::keyringstorage::KeyringStorage;
|
||||
use super::keys::{KeyStorage, KeyStorageError};
|
||||
// Local
|
||||
use crate::config::{
|
||||
bookmarks::{Bookmark, UserHosts},
|
||||
serialization::{SerializerError, SerializerErrorKind, deserialize, serialize},
|
||||
};
|
||||
use crate::config::bookmarks::{Bookmark, UserHosts};
|
||||
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::utils::crypto;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
@@ -104,7 +99,7 @@ impl BookmarksClient {
|
||||
fn keyring(storage_path: &Path, keyring: bool) -> (Box<dyn KeyStorage>, &'static str) {
|
||||
if keyring && cfg!(feature = "keyring") {
|
||||
debug!("Setting up KeyStorage");
|
||||
let username: String = whoami::username();
|
||||
let username = whoami::username().unwrap_or_default();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
// Check if keyring storage is supported
|
||||
#[cfg(not(test))]
|
||||
|
||||
@@ -78,14 +78,13 @@ impl KeyStorage for KeyringStorage {
|
||||
mod tests {
|
||||
#[test]
|
||||
#[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))]
|
||||
fn test_system_keys_keyringstorage() {
|
||||
fn test_system_keys_keyring_storage() {
|
||||
use pretty_assertions::assert_eq;
|
||||
use whoami::username;
|
||||
|
||||
use super::*;
|
||||
|
||||
let username: String = username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
let username = whoami::username().expect("no username");
|
||||
let storage = KeyringStorage::new(username.as_str());
|
||||
assert!(storage.is_supported());
|
||||
let app_name: &str = "termscp-test2";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::path::PathBuf;
|
||||
|
||||
use remotefs::File;
|
||||
|
||||
use super::{FileExplorerTab, FileTransferActivity, LogLevel, Msg, PendingActionMsg};
|
||||
use super::{FileTransferActivity, LogLevel, Msg, PendingActionMsg};
|
||||
|
||||
/// Describes destination for sync browsing
|
||||
enum SyncBrowsingDestination {
|
||||
@@ -17,47 +17,29 @@ enum SyncBrowsingDestination {
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Enter a directory on local host from entry
|
||||
pub(crate) fn action_enter_local_dir(&mut self, dir: File) {
|
||||
self.host_bridge_changedir(dir.path(), true);
|
||||
/// Enter a directory from entry, dispatching via the active tab's pane.
|
||||
pub(crate) fn action_enter_dir(&mut self, dir: File) {
|
||||
self.pane_changedir(dir.path(), true);
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Enter a directory on local host from entry
|
||||
pub(crate) fn action_enter_remote_dir(&mut self, dir: File) {
|
||||
self.remote_changedir(dir.path(), true);
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Change local directory reading value from input
|
||||
pub(crate) fn action_change_local_dir(&mut self, input: String) {
|
||||
let dir_path: PathBuf =
|
||||
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.host_bridge_changedir(dir_path.as_path(), true);
|
||||
/// Change directory reading value from input, dispatching via the active tab's pane.
|
||||
pub(crate) fn action_change_dir(&mut self, input: String) {
|
||||
let dir_path = self.pane_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.pane_changedir(dir_path.as_path(), true);
|
||||
// Check whether to sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
||||
}
|
||||
}
|
||||
|
||||
/// Change remote directory reading value from input
|
||||
pub(crate) fn action_change_remote_dir(&mut self, input: String) {
|
||||
let dir_path: PathBuf = self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.remote_changedir(dir_path.as_path(), true);
|
||||
// Check whether to sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
||||
}
|
||||
}
|
||||
|
||||
/// Go to previous directory from localhost
|
||||
pub(crate) fn action_go_to_previous_local_dir(&mut self) {
|
||||
if let Some(d) = self.host_bridge_mut().popd() {
|
||||
self.host_bridge_changedir(d.as_path(), false);
|
||||
/// Go to previous directory, dispatching via the active tab's pane.
|
||||
pub(crate) fn action_go_to_previous_dir(&mut self) {
|
||||
let prev = self.browser.fs_pane_mut().explorer.popd();
|
||||
if let Some(d) = prev {
|
||||
self.pane_changedir(d.as_path(), false);
|
||||
// Check whether to sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
||||
@@ -65,41 +47,12 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Go to previous directory from remote host
|
||||
pub(crate) fn action_go_to_previous_remote_dir(&mut self) {
|
||||
if let Some(d) = self.remote_mut().popd() {
|
||||
self.remote_changedir(d.as_path(), false);
|
||||
// Check whether to sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Go to upper directory on local host
|
||||
pub(crate) fn action_go_to_local_upper_dir(&mut self) {
|
||||
// Get pwd
|
||||
let path: PathBuf = self.host_bridge().wrkdir.clone();
|
||||
// Go to parent directory
|
||||
/// Go to upper directory, dispatching via the active tab's pane.
|
||||
pub(crate) fn action_go_to_upper_dir(&mut self) {
|
||||
let path = self.browser.fs_pane().explorer.wrkdir.clone();
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.host_bridge_changedir(parent, true);
|
||||
// If sync is enabled update remote too
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #### action_go_to_remote_upper_dir
|
||||
///
|
||||
/// Go to upper directory on remote host
|
||||
pub(crate) fn action_go_to_remote_upper_dir(&mut self) {
|
||||
// Get pwd
|
||||
let path: PathBuf = self.remote().wrkdir.clone();
|
||||
// Go to parent directory
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.remote_changedir(parent, true);
|
||||
// If sync is enabled update local too
|
||||
self.pane_changedir(parent, true);
|
||||
// If sync is enabled update the other side too
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
||||
}
|
||||
@@ -117,9 +70,11 @@ impl FileTransferActivity {
|
||||
None => return,
|
||||
};
|
||||
trace!("Synchronizing browsing to path {}", path.display());
|
||||
// Check whether destination exists on host
|
||||
let exists = match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => match self.client.exists(path.as_path()) {
|
||||
// Check whether destination exists on the opposite side
|
||||
let is_local = self.is_local_tab();
|
||||
let exists = if is_local {
|
||||
// Current tab is local, so the opposite is remote
|
||||
match self.browser.remote_pane_mut().fs.exists(path.as_path()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
error!(
|
||||
@@ -129,8 +84,10 @@ impl FileTransferActivity {
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
FileExplorerTab::Remote => match self.host_bridge.exists(path.as_path()) {
|
||||
}
|
||||
} else {
|
||||
// Current tab is remote, so the opposite is local
|
||||
match self.browser.local_pane_mut().fs.exists(path.as_path()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
error!(
|
||||
@@ -140,8 +97,7 @@ impl FileTransferActivity {
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
_ => return,
|
||||
}
|
||||
};
|
||||
let name = path
|
||||
.file_name()
|
||||
@@ -159,12 +115,8 @@ impl FileTransferActivity {
|
||||
]) == Msg::PendingAction(PendingActionMsg::MakePendingDirectory)
|
||||
{
|
||||
trace!("User wants to create the unexisting directory");
|
||||
// Make directory
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_remote_mkdir(name.clone()),
|
||||
FileExplorerTab::Remote => self.action_local_mkdir(name.clone()),
|
||||
_ => {}
|
||||
}
|
||||
// Make directory on the opposite side
|
||||
self.sync_mkdir_on_opposite(name.clone());
|
||||
} else {
|
||||
// Do not synchronize, disable sync browsing and return
|
||||
trace!(
|
||||
@@ -183,23 +135,21 @@ impl FileTransferActivity {
|
||||
self.umount_sync_browsing_mkdir_popup();
|
||||
}
|
||||
trace!("Entering on the other explorer directory {}", name);
|
||||
// Enter directory
|
||||
match destination {
|
||||
SyncBrowsingDestination::ParentDir => match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
||||
_ => {}
|
||||
},
|
||||
SyncBrowsingDestination::Path(_) => match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
||||
_ => {}
|
||||
},
|
||||
SyncBrowsingDestination::PreviousDir => match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), false),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), false),
|
||||
_ => {}
|
||||
},
|
||||
// Enter directory on the opposite side
|
||||
let push = !matches!(destination, SyncBrowsingDestination::PreviousDir);
|
||||
if is_local {
|
||||
self.remote_changedir(path.as_path(), push);
|
||||
} else {
|
||||
self.local_changedir(path.as_path(), push);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a directory on the opposite side for sync browsing.
|
||||
fn sync_mkdir_on_opposite(&mut self, name: String) {
|
||||
if self.is_local_tab() {
|
||||
self.action_remote_mkdir(name);
|
||||
} else {
|
||||
self.action_local_mkdir(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,35 +158,34 @@ impl FileTransferActivity {
|
||||
&mut self,
|
||||
destination: &SyncBrowsingDestination,
|
||||
) -> Option<PathBuf> {
|
||||
match (destination, self.browser.tab()) {
|
||||
// NOTE: tab and methods are switched on purpose
|
||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::HostBridge) => {
|
||||
self.remote().wrkdir.parent().map(|x| x.to_path_buf())
|
||||
}
|
||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => {
|
||||
self.host_bridge().wrkdir.parent().map(|x| x.to_path_buf())
|
||||
}
|
||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::HostBridge) => {
|
||||
if let Some(p) = self.remote_mut().popd() {
|
||||
Some(p)
|
||||
let is_local = self.is_local_tab();
|
||||
match destination {
|
||||
// NOTE: tab and methods are switched on purpose (we resolve from the opposite side)
|
||||
SyncBrowsingDestination::ParentDir => {
|
||||
if is_local {
|
||||
self.remote().wrkdir.parent().map(|x| x.to_path_buf())
|
||||
} else {
|
||||
warn!("Cannot synchronize browsing: remote has no previous directory in stack");
|
||||
None
|
||||
self.host_bridge().wrkdir.parent().map(|x| x.to_path_buf())
|
||||
}
|
||||
}
|
||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => {
|
||||
if let Some(p) = self.host_bridge_mut().popd() {
|
||||
SyncBrowsingDestination::PreviousDir => {
|
||||
if is_local {
|
||||
if let Some(p) = self.remote_mut().popd() {
|
||||
Some(p)
|
||||
} else {
|
||||
warn!(
|
||||
"Cannot synchronize browsing: remote has no previous directory in stack"
|
||||
);
|
||||
None
|
||||
}
|
||||
} else if let Some(p) = self.host_bridge_mut().popd() {
|
||||
Some(p)
|
||||
} else {
|
||||
warn!("Cannot synchronize browsing: local has no previous directory in stack");
|
||||
None
|
||||
}
|
||||
}
|
||||
(SyncBrowsingDestination::Path(p), _) => Some(PathBuf::from(p.as_str())),
|
||||
_ => {
|
||||
warn!("Cannot synchronize browsing for current explorer");
|
||||
None
|
||||
}
|
||||
SyncBrowsingDestination::Path(p) => Some(PathBuf::from(p.as_str())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,83 +3,12 @@ use remotefs::fs::UnixPex;
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub fn action_local_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_local_selected_entries().get_files();
|
||||
/// Change file mode for the currently selected entries via the active tab's pane.
|
||||
pub(crate) fn action_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_selected_entries().get_files();
|
||||
|
||||
for file in files {
|
||||
if let Err(err) = self.host_bridge.chmod(file.path(), mode) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"could not change mode for {}: {}",
|
||||
file.path().display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_remote_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_remote_selected_entries().get_files();
|
||||
|
||||
for file in files {
|
||||
let mut metadata = file.metadata.clone();
|
||||
metadata.mode = Some(mode);
|
||||
|
||||
if let Err(err) = self.client.setstat(file.path(), metadata) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"could not change mode for {}: {}",
|
||||
file.path().display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_find_local_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_found_selected_entries().get_files();
|
||||
|
||||
for file in files {
|
||||
if let Err(err) = self.host_bridge.chmod(file.path(), mode) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"could not change mode for {}: {}",
|
||||
file.path().display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("changed mode to {:#o} for {}", u32::from(mode), file.name()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_find_remote_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_found_selected_entries().get_files();
|
||||
|
||||
for file in files {
|
||||
let mut metadata = file.metadata.clone();
|
||||
metadata.mode = Some(mode);
|
||||
|
||||
if let Err(err) = self.client.setstat(file.path(), metadata) {
|
||||
if let Err(err) = self.browser.fs_pane_mut().fs.chmod(file.path(), mode) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
|
||||
@@ -1,61 +1,38 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use remotefs::{File, RemoteErrorType};
|
||||
use remotefs::File;
|
||||
|
||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Copy file on local
|
||||
pub(crate) fn action_local_copy(&mut self, input: String) {
|
||||
match self.get_local_selected_entries() {
|
||||
/// Copy the currently selected file(s) via the active tab's pane.
|
||||
pub(crate) fn action_copy(&mut self, input: String) {
|
||||
match self.get_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
let dest_path: PathBuf = PathBuf::from(input);
|
||||
self.local_copy_file(&entry, dest_path.as_path());
|
||||
let dest_path = PathBuf::from(input);
|
||||
self.copy_file(entry, dest_path.as_path());
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for (entry, mut dest_path) in entries.into_iter() {
|
||||
dest_path.push(entry.name());
|
||||
self.local_copy_file(&entry, dest_path.as_path());
|
||||
self.copy_file(entry, dest_path.as_path());
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
// clear selection and reload
|
||||
self.browser.explorer_mut().clear_queue();
|
||||
self.reload_browser_file_list();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy file on remote
|
||||
pub(crate) fn action_remote_copy(&mut self, input: String) {
|
||||
match self.get_remote_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
let dest_path: PathBuf = PathBuf::from(input);
|
||||
self.remote_copy_file(entry, dest_path.as_path());
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for (entry, mut dest_path) in entries.into_iter() {
|
||||
dest_path.push(entry.name());
|
||||
self.remote_copy_file(entry, dest_path.as_path());
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn local_copy_file(&mut self, entry: &File, dest: &Path) {
|
||||
match self.host_bridge.copy(entry, dest) {
|
||||
fn copy_file(&mut self, entry: File, dest: &Path) {
|
||||
match self.browser.fs_pane_mut().fs.copy(&entry, dest) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
@@ -66,45 +43,23 @@ impl FileTransferActivity {
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not copy \"{}\" to \"{}\": {}",
|
||||
entry.path().display(),
|
||||
dest.display(),
|
||||
err
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_copy_file(&mut self, entry: File, dest: &Path) {
|
||||
match self.client.as_mut().copy(entry.path(), dest) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Copied \"{}\" to \"{}\"",
|
||||
entry.path().display(),
|
||||
dest.display()
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(err) => match err.kind {
|
||||
RemoteErrorType::UnsupportedFeature => {
|
||||
// If copy is not supported, perform the tricky copy
|
||||
Err(err) => {
|
||||
// On remote tabs, fall back to tricky_copy (download + re-upload)
|
||||
// when the protocol doesn't support server-side copy.
|
||||
if !self.is_local_tab() {
|
||||
let _ = self.tricky_copy(entry, dest);
|
||||
} else {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not copy \"{}\" to \"{}\": {}",
|
||||
entry.path().display(),
|
||||
dest.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
}
|
||||
_ => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not copy \"{}\" to \"{}\": {}",
|
||||
entry.path().display(),
|
||||
dest.display(),
|
||||
err
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +93,12 @@ impl FileTransferActivity {
|
||||
return Err(err);
|
||||
}
|
||||
// Stat dir
|
||||
let tempdir_entry = match self.host_bridge.stat(tempdir_path.as_path()) {
|
||||
let tempdir_entry = match self
|
||||
.browser
|
||||
.local_pane_mut()
|
||||
.fs
|
||||
.stat(tempdir_path.as_path())
|
||||
{
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
@@ -191,9 +151,17 @@ impl FileTransferActivity {
|
||||
return Err(err);
|
||||
}
|
||||
// Get local fs entry
|
||||
let tmpfile_entry = match self.host_bridge.stat(tmpfile.path()) {
|
||||
let tmpfile_entry = match self.browser.local_pane_mut().fs.stat(tmpfile.path()) {
|
||||
Ok(e) if e.is_file() => e,
|
||||
Ok(_) => panic!("{} is not a file", tmpfile.path().display()),
|
||||
Ok(_) => {
|
||||
let msg = format!(
|
||||
"Copy failed: \"{}\" is not a file",
|
||||
tmpfile.path().display()
|
||||
);
|
||||
error!("{msg}");
|
||||
self.log_and_alert(LogLevel::Error, msg.clone());
|
||||
return Err(msg);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use remotefs::File;
|
||||
@@ -8,72 +8,28 @@ use remotefs::File;
|
||||
use super::{FileTransferActivity, LogLevel, SelectedFile};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_delete(&mut self) {
|
||||
match self.get_local_selected_entries() {
|
||||
/// Delete the currently selected file(s) via the active tab's pane.
|
||||
pub(crate) fn action_delete(&mut self) {
|
||||
match self.get_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
// Delete file
|
||||
self.local_remove_file(&entry);
|
||||
self.remove_file(&entry);
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for (entry, _) in entries.iter() {
|
||||
// Delete file
|
||||
self.local_remove_file(entry);
|
||||
self.remove_file(entry);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
// clear selection and reload
|
||||
self.browser.explorer_mut().clear_queue();
|
||||
self.reload_browser_file_list();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_remote_delete(&mut self) {
|
||||
match self.get_remote_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
// Delete file
|
||||
self.remote_remove_file(&entry);
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for (entry, _) in entries.iter() {
|
||||
// Delete file
|
||||
self.remote_remove_file(entry);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn local_remove_file(&mut self, entry: &File) {
|
||||
match self.host_bridge.remove(entry) {
|
||||
Ok(_) => {
|
||||
// Log
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Removed file \"{}\"", entry.path().display()),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not delete file \"{}\": {}",
|
||||
entry.path().display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remote_remove_file(&mut self, entry: &File) {
|
||||
match self.client.remove_dir_all(entry.path()) {
|
||||
/// Remove a single file or directory via the active tab's pane.
|
||||
pub(crate) fn remove_file(&mut self, entry: &File) {
|
||||
match self.browser.fs_pane_mut().fs.remove(entry) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
@@ -10,6 +10,7 @@ use std::time::SystemTime;
|
||||
use remotefs::File;
|
||||
use remotefs::fs::Metadata;
|
||||
|
||||
use super::super::ui_result;
|
||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||
|
||||
impl FileTransferActivity {
|
||||
@@ -28,7 +29,7 @@ impl FileTransferActivity {
|
||||
format!("Opening file \"{}\"…", entry.path().display()),
|
||||
);
|
||||
// Edit file
|
||||
let res = match self.host_bridge.is_localhost() {
|
||||
let res = match self.browser.local_pane().fs.is_localhost() {
|
||||
true => self.edit_local_file(entry.path()).map(|_| ()),
|
||||
false => self.edit_bridged_local_file(entry),
|
||||
};
|
||||
@@ -88,7 +89,7 @@ impl FileTransferActivity {
|
||||
};
|
||||
|
||||
// open from host bridge
|
||||
let mut reader = match self.host_bridge.open_file(entry.path()) {
|
||||
let mut reader = match self.browser.local_pane_mut().fs.open_file(entry.path()) {
|
||||
Ok(reader) => reader,
|
||||
Err(err) => {
|
||||
return Err(format!("Failed to open bridged entry: {err}"));
|
||||
@@ -122,7 +123,7 @@ impl FileTransferActivity {
|
||||
return Err(format!("Could not open file: {err}"));
|
||||
}
|
||||
};
|
||||
let mut writer = match self.host_bridge.create_file(
|
||||
let mut writer = match self.browser.local_pane_mut().fs.create_file(
|
||||
entry.path(),
|
||||
&Metadata {
|
||||
size: new_file_size,
|
||||
@@ -139,7 +140,9 @@ impl FileTransferActivity {
|
||||
return Err(format!("Could not write file: {err}"));
|
||||
}
|
||||
|
||||
self.host_bridge
|
||||
self.browser
|
||||
.local_pane_mut()
|
||||
.fs
|
||||
.finalize_write(writer)
|
||||
.map_err(|err| format!("Could not write file: {err}"))?;
|
||||
}
|
||||
@@ -177,7 +180,7 @@ impl FileTransferActivity {
|
||||
error!("Could not leave alternate screen: {}", err);
|
||||
}
|
||||
// Lock ports
|
||||
assert!(self.app.lock_ports().is_ok());
|
||||
ui_result(self.app.lock_ports());
|
||||
// Get current file modification time
|
||||
let prev_mtime = self.get_localhost_mtime(path)?;
|
||||
// Open editor
|
||||
@@ -205,7 +208,7 @@ impl FileTransferActivity {
|
||||
error!("Could not clear screen screen: {}", err);
|
||||
}
|
||||
// Unlock ports
|
||||
assert!(self.app.unlock_ports().is_ok());
|
||||
ui_result(self.app.unlock_ports());
|
||||
}
|
||||
let after_mtime = self.get_localhost_mtime(path)?;
|
||||
|
||||
@@ -241,7 +244,8 @@ impl FileTransferActivity {
|
||||
return Err(format!("Could not open file {file_name}: {err}"));
|
||||
}
|
||||
// Get current file modification time
|
||||
let prev_mtime: SystemTime = match self.host_bridge.stat(tmpfile.as_path()) {
|
||||
let prev_mtime: SystemTime = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path())
|
||||
{
|
||||
Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH),
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
@@ -254,7 +258,7 @@ impl FileTransferActivity {
|
||||
// Edit file
|
||||
self.edit_local_file(tmpfile.as_path())?;
|
||||
// Get local fs entry
|
||||
let tmpfile_entry: File = match self.host_bridge.stat(tmpfile.as_path()) {
|
||||
let tmpfile_entry: File = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
@@ -280,7 +284,7 @@ impl FileTransferActivity {
|
||||
),
|
||||
);
|
||||
// Get local fs entry
|
||||
let tmpfile_entry = match self.host_bridge.stat(tmpfile.as_path()) {
|
||||
let tmpfile_entry = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
@@ -37,15 +37,11 @@ impl FromStr for Command {
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_exec(&mut self, input: String) {
|
||||
self.action_exec(false, input);
|
||||
pub(crate) fn action_exec_cmd(&mut self, input: String) {
|
||||
self.action_exec(input);
|
||||
}
|
||||
|
||||
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
||||
self.action_exec(true, input);
|
||||
}
|
||||
|
||||
fn action_exec(&mut self, remote: bool, cmd: String) {
|
||||
fn action_exec(&mut self, cmd: String) {
|
||||
if cmd.is_empty() {
|
||||
self.print_terminal("".to_string());
|
||||
}
|
||||
@@ -61,10 +57,10 @@ impl FileTransferActivity {
|
||||
|
||||
match cmd {
|
||||
Command::Cd(path) => {
|
||||
self.action_exec_cd(remote, path);
|
||||
self.action_exec_cd(path);
|
||||
}
|
||||
Command::Exec(executable) => {
|
||||
self.action_exec_executable(remote, executable);
|
||||
self.action_exec_executable(executable);
|
||||
}
|
||||
Command::Exit => {
|
||||
self.action_exec_exit();
|
||||
@@ -77,43 +73,20 @@ impl FileTransferActivity {
|
||||
self.umount_exec();
|
||||
}
|
||||
|
||||
fn action_exec_cd(&mut self, remote: bool, input: String) {
|
||||
let new_dir = if remote {
|
||||
let dir_path: PathBuf =
|
||||
self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.remote_changedir(dir_path.as_path(), true);
|
||||
|
||||
dir_path
|
||||
} else {
|
||||
let dir_path: PathBuf =
|
||||
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.host_bridge_changedir(dir_path.as_path(), true);
|
||||
|
||||
dir_path
|
||||
};
|
||||
fn action_exec_cd(&mut self, input: String) {
|
||||
let dir_path = self.pane_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.pane_changedir(dir_path.as_path(), true);
|
||||
|
||||
self.update_browser_file_list();
|
||||
|
||||
// update prompt and print the new directory
|
||||
self.update_terminal_prompt();
|
||||
self.print_terminal(new_dir.display().to_string());
|
||||
self.print_terminal(dir_path.display().to_string());
|
||||
}
|
||||
|
||||
/// Execute a [`Command::Exec`] command
|
||||
fn action_exec_executable(&mut self, remote: bool, cmd: String) {
|
||||
let res = if remote {
|
||||
self.client
|
||||
.as_mut()
|
||||
.exec(cmd.as_str())
|
||||
.map(|(_, output)| output)
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
self.host_bridge
|
||||
.exec(cmd.as_str())
|
||||
.map_err(|e| e.to_string())
|
||||
};
|
||||
|
||||
match res {
|
||||
/// Execute a command via the active tab's pane.
|
||||
fn action_exec_executable(&mut self, cmd: String) {
|
||||
match self.browser.fs_pane_mut().fs.exec(cmd.as_str()) {
|
||||
Ok(output) => {
|
||||
self.print_terminal(output);
|
||||
}
|
||||
@@ -122,7 +95,7 @@ impl FileTransferActivity {
|
||||
LogLevel::Error,
|
||||
format!("Could not execute command \"{cmd}\": {err}"),
|
||||
);
|
||||
self.print_terminal(err);
|
||||
self.print_terminal(err.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,14 @@
|
||||
use remotefs::File;
|
||||
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum Host {
|
||||
HostBridge,
|
||||
Remote,
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Calculate and display the total size of the selected file(s) via the active tab's pane.
|
||||
pub(crate) fn action_get_file_size(&mut self) {
|
||||
// Get selected file
|
||||
self.mount_blocking_wait("Getting total path size...");
|
||||
|
||||
let total_size = match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => {
|
||||
let files = self.get_local_selected_entries().get_files();
|
||||
self.get_files_size(files, Host::HostBridge)
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
let files = self.get_remote_selected_entries().get_files();
|
||||
self.get_files_size(files, Host::Remote)
|
||||
}
|
||||
FileExplorerTab::FindHostBridge => {
|
||||
let files = self.get_found_selected_entries().get_files();
|
||||
self.get_files_size(files, Host::HostBridge)
|
||||
}
|
||||
FileExplorerTab::FindRemote => {
|
||||
let files = self.get_found_selected_entries().get_files();
|
||||
self.get_files_size(files, Host::Remote)
|
||||
}
|
||||
};
|
||||
let files = self.get_selected_entries().get_files();
|
||||
let total_size = self.get_files_size(files);
|
||||
|
||||
self.umount_wait();
|
||||
self.mount_info(format!(
|
||||
@@ -40,24 +17,19 @@ impl FileTransferActivity {
|
||||
));
|
||||
}
|
||||
|
||||
fn get_files_size(&mut self, files: Vec<File>, host: Host) -> u64 {
|
||||
files.into_iter().map(|f| self.get_file_size(f, host)).sum()
|
||||
fn get_files_size(&mut self, files: Vec<File>) -> u64 {
|
||||
files.into_iter().map(|f| self.get_file_size(f)).sum()
|
||||
}
|
||||
|
||||
fn get_file_size(&mut self, file: File, host: Host) -> u64 {
|
||||
fn get_file_size(&mut self, file: File) -> u64 {
|
||||
if let Some(symlink) = &file.metadata().symlink {
|
||||
// stat
|
||||
let stat_res = match host {
|
||||
Host::HostBridge => self.host_bridge.stat(symlink).map_err(|e| e.to_string()),
|
||||
Host::Remote => self.client.stat(symlink).map_err(|e| e.to_string()),
|
||||
};
|
||||
match stat_res {
|
||||
match self.browser.fs_pane_mut().fs.stat(symlink) {
|
||||
Ok(stat) => stat.metadata().size,
|
||||
Err(err_msg) => {
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Failed to stat symlink target {path}: {err_msg}",
|
||||
"Failed to stat symlink target {path}: {err}",
|
||||
path = symlink.display(),
|
||||
),
|
||||
);
|
||||
@@ -65,22 +37,13 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
} else if file.is_dir() {
|
||||
// list and sum
|
||||
let list_res = match host {
|
||||
Host::HostBridge => self
|
||||
.host_bridge
|
||||
.list_dir(&file.path)
|
||||
.map_err(|e| e.to_string()),
|
||||
Host::Remote => self.client.list_dir(&file.path).map_err(|e| e.to_string()),
|
||||
};
|
||||
|
||||
match list_res {
|
||||
Ok(list) => list.into_iter().map(|f| self.get_file_size(f, host)).sum(),
|
||||
Err(err_msg) => {
|
||||
match self.browser.fs_pane_mut().fs.list_dir(&file.path) {
|
||||
Ok(list) => list.into_iter().map(|f| self.get_file_size(f)).sum(),
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Failed to list directory {path}: {err_msg}",
|
||||
"Failed to list directory {path}: {err}",
|
||||
path = file.path.display(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::super::browser::FileExplorerTab;
|
||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
||||
|
||||
impl FileTransferActivity {
|
||||
@@ -22,33 +21,19 @@ impl FileTransferActivity {
|
||||
Some(p) => p.to_path_buf(),
|
||||
}
|
||||
};
|
||||
// Change directory
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.host_bridge_changedir(path.as_path(), true)
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
self.remote_changedir(path.as_path(), true)
|
||||
}
|
||||
}
|
||||
// Change directory on the active tab's pane
|
||||
self.pane_changedir(path.as_path(), true);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
|
||||
let wrkdir: PathBuf = match self.browser.tab() {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.remote().wrkdir.clone()
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
self.host_bridge().wrkdir.clone()
|
||||
}
|
||||
};
|
||||
let wrkdir: PathBuf = self.browser.opposite_pane().explorer.wrkdir.clone();
|
||||
match self.get_found_selected_entries() {
|
||||
SelectedFile::One(entry) => match self.browser.tab() {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
SelectedFile::One(entry) => {
|
||||
if self.is_local_tab() {
|
||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||
if self.config().get_prompt_on_file_replace()
|
||||
&& self.remote_file_exists(file_to_check.as_path())
|
||||
&& self.file_exists(file_to_check.as_path(), false)
|
||||
&& !self.should_replace_file(
|
||||
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
||||
)
|
||||
@@ -66,11 +51,10 @@ impl FileTransferActivity {
|
||||
format!("Could not upload file: {err}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
} else {
|
||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||
if self.config().get_prompt_on_file_replace()
|
||||
&& self.host_bridge_file_exists(file_to_check.as_path())
|
||||
&& self.file_exists(file_to_check.as_path(), true)
|
||||
&& !self.should_replace_file(
|
||||
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
||||
)
|
||||
@@ -89,7 +73,7 @@ impl FileTransferActivity {
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// In case of selection: save multiple files in wrkdir/input
|
||||
let mut dest_path: PathBuf = wrkdir;
|
||||
@@ -97,58 +81,51 @@ impl FileTransferActivity {
|
||||
dest_path.push(save_as);
|
||||
}
|
||||
// Iter files
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
||||
entries,
|
||||
) = self.get_files_to_transfer_with_overwrites(
|
||||
if self.is_local_tab() {
|
||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
|
||||
self.get_files_to_transfer_with_overwrites(
|
||||
entries,
|
||||
super::save::CheckFileExists::Remote,
|
||||
)
|
||||
else {
|
||||
debug!("User cancelled file transfer due to overwrites");
|
||||
return;
|
||||
};
|
||||
if let Err(err) = self.filetransfer_send(
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
{
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not upload file: {err}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
debug!("User cancelled file transfer due to overwrites");
|
||||
return;
|
||||
};
|
||||
if let Err(err) = self.filetransfer_send(
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not upload file: {err}"),
|
||||
);
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
||||
entries,
|
||||
) = self.get_files_to_transfer_with_overwrites(
|
||||
} else {
|
||||
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
|
||||
self.get_files_to_transfer_with_overwrites(
|
||||
entries,
|
||||
super::save::CheckFileExists::HostBridge,
|
||||
)
|
||||
else {
|
||||
debug!("User cancelled file transfer due to overwrites");
|
||||
return;
|
||||
};
|
||||
if let Err(err) = self.filetransfer_recv(
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not download file: {err}"),
|
||||
);
|
||||
}
|
||||
else {
|
||||
debug!("User cancelled file transfer due to overwrites");
|
||||
return;
|
||||
};
|
||||
if let Err(err) = self.filetransfer_recv(
|
||||
TransferPayload::TransferQueue(entries),
|
||||
dest_path.as_path(),
|
||||
None,
|
||||
) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not download file: {err}"),
|
||||
);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
if let Some(f) = self.found_mut() {
|
||||
f.clear_queue();
|
||||
self.update_find_list();
|
||||
}
|
||||
// clear selection
|
||||
if let Some(f) = self.found_mut() {
|
||||
f.clear_queue();
|
||||
self.update_find_list();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,14 +136,11 @@ impl FileTransferActivity {
|
||||
pub(crate) fn action_find_delete(&mut self) {
|
||||
match self.get_found_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
// Delete file
|
||||
self.remove_found_file(&entry);
|
||||
self.remove_file(&entry);
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for (entry, _) in entries.iter() {
|
||||
// Delete file
|
||||
self.remove_found_file(entry);
|
||||
self.remove_file(entry);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
@@ -179,17 +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) {
|
||||
match self.get_found_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
@@ -235,13 +198,6 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
fn open_found_file(&mut self, entry: &File, with: Option<&str>) {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||
self.action_open_local_file(entry, with);
|
||||
}
|
||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||
self.action_open_remote_file(entry, with);
|
||||
}
|
||||
}
|
||||
self.open_file(entry, with);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
use super::FileTransferActivity;
|
||||
|
||||
|
||||
@@ -1,49 +1,46 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::path::PathBuf;
|
||||
|
||||
use remotefs::fs::UnixPex;
|
||||
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_mkdir(&mut self, input: String) {
|
||||
match self
|
||||
.host_bridge
|
||||
.mkdir(PathBuf::from(input.as_str()).as_path())
|
||||
{
|
||||
Ok(_) => {
|
||||
// Reload files
|
||||
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
||||
}
|
||||
Err(err) => {
|
||||
// Report err
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create directory \"{input}\": {err}"),
|
||||
);
|
||||
}
|
||||
/// Create a directory via the active tab's pane.
|
||||
pub(crate) fn action_mkdir(&mut self, input: String) {
|
||||
let path = PathBuf::from(input.as_str());
|
||||
match self.browser.fs_pane_mut().fs.mkdir(path.as_path()) {
|
||||
Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")),
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create directory \"{input}\": {err}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
pub(crate) fn action_remote_mkdir(&mut self, input: String) {
|
||||
match self.client.as_mut().create_dir(
|
||||
PathBuf::from(input.as_str()).as_path(),
|
||||
UnixPex::from(0o755),
|
||||
) {
|
||||
Ok(_) => {
|
||||
// Reload files
|
||||
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
||||
}
|
||||
Err(err) => {
|
||||
// Report err
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create directory \"{input}\": {err}"),
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a directory on the local host (used by sync-browsing in change_dir).
|
||||
pub(in crate::ui::activities::filetransfer) fn action_local_mkdir(&mut self, input: String) {
|
||||
let path = PathBuf::from(input.as_str());
|
||||
match self.browser.local_pane_mut().fs.mkdir(path.as_path()) {
|
||||
Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")),
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create directory \"{input}\": {err}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a directory on the remote host (used by sync-browsing in change_dir).
|
||||
pub(in crate::ui::activities::filetransfer) fn action_remote_mkdir(&mut self, input: String) {
|
||||
let path = PathBuf::from(input.as_str());
|
||||
match self.browser.remote_pane_mut().fs.mkdir(path.as_path()) {
|
||||
Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")),
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create directory \"{input}\": {err}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -92,36 +92,43 @@ impl FileTransferActivity {
|
||||
self.get_selected_files(&Id::ExplorerHostBridge)
|
||||
}
|
||||
|
||||
pub(crate) fn get_local_selected_file(&self) -> Option<File> {
|
||||
self.get_selected_file(&Id::ExplorerHostBridge)
|
||||
}
|
||||
|
||||
/// Get remote file entry
|
||||
pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile {
|
||||
self.get_selected_files(&Id::ExplorerRemote)
|
||||
}
|
||||
|
||||
pub(crate) fn get_remote_selected_file(&self) -> Option<File> {
|
||||
self.get_selected_file(&Id::ExplorerRemote)
|
||||
}
|
||||
|
||||
/// Returns whether only one entry is selected on local host
|
||||
pub(crate) fn is_local_selected_one(&mut self) -> bool {
|
||||
matches!(self.get_local_selected_entries(), SelectedFile::One(_))
|
||||
}
|
||||
|
||||
/// Returns whether only one entry is selected on remote host
|
||||
pub(crate) fn is_remote_selected_one(&mut self) -> bool {
|
||||
matches!(self.get_remote_selected_entries(), SelectedFile::One(_))
|
||||
}
|
||||
|
||||
/// Get remote file entry
|
||||
pub(crate) fn get_found_selected_entries(&mut self) -> SelectedFile {
|
||||
self.get_selected_files(&Id::ExplorerFind)
|
||||
}
|
||||
|
||||
pub(crate) fn get_found_selected_file(&self) -> Option<File> {
|
||||
self.get_selected_file(&Id::ExplorerFind)
|
||||
self.get_selected_file_by_id(&Id::ExplorerFind)
|
||||
}
|
||||
|
||||
/// Get selected entries from whichever tab is currently active.
|
||||
pub(crate) fn get_selected_entries(&mut self) -> SelectedFile {
|
||||
let id = match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => Id::ExplorerHostBridge,
|
||||
FileExplorerTab::Remote => Id::ExplorerRemote,
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => Id::ExplorerFind,
|
||||
};
|
||||
self.get_selected_files(&id)
|
||||
}
|
||||
|
||||
/// Get single selected file from whichever tab is currently active.
|
||||
pub(crate) fn get_selected_file(&self) -> Option<File> {
|
||||
let id = match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => Id::ExplorerHostBridge,
|
||||
FileExplorerTab::Remote => Id::ExplorerRemote,
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => Id::ExplorerFind,
|
||||
};
|
||||
self.get_selected_file_by_id(&id)
|
||||
}
|
||||
|
||||
/// Returns whether only one entry is selected on the current tab.
|
||||
pub(crate) fn is_selected_one(&mut self) -> bool {
|
||||
matches!(self.get_selected_entries(), SelectedFile::One(_))
|
||||
}
|
||||
|
||||
// -- private
|
||||
@@ -164,13 +171,13 @@ impl FileTransferActivity {
|
||||
|
||||
fn get_file_from_path(&mut self, id: &Id, path: &Path) -> Option<File> {
|
||||
match *id {
|
||||
Id::ExplorerHostBridge => self.host_bridge.stat(path).ok(),
|
||||
Id::ExplorerRemote => self.client.stat(path).ok(),
|
||||
Id::ExplorerHostBridge => self.browser.local_pane_mut().fs.stat(path).ok(),
|
||||
Id::ExplorerRemote => self.browser.remote_pane_mut().fs.stat(path).ok(),
|
||||
Id::ExplorerFind => {
|
||||
let found = self.browser.found_tab().unwrap();
|
||||
match found {
|
||||
FoundExplorerTab::Local => self.host_bridge.stat(path).ok(),
|
||||
FoundExplorerTab::Remote => self.client.stat(path).ok(),
|
||||
FoundExplorerTab::Local => self.browser.local_pane_mut().fs.stat(path).ok(),
|
||||
FoundExplorerTab::Remote => self.browser.remote_pane_mut().fs.stat(path).ok(),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
@@ -182,11 +189,16 @@ impl FileTransferActivity {
|
||||
Id::ExplorerHostBridge => self.host_bridge(),
|
||||
Id::ExplorerRemote => self.remote(),
|
||||
Id::ExplorerFind => self.found().as_ref().unwrap(),
|
||||
_ => unreachable!(),
|
||||
_ => {
|
||||
error!(
|
||||
"browser_by_id called with unexpected id: {id:?}; falling back to local explorer"
|
||||
);
|
||||
self.host_bridge()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_file(&self, id: &Id) -> Option<File> {
|
||||
fn get_selected_file_by_id(&self, id: &Id) -> Option<File> {
|
||||
let browser = self.browser_by_id(id);
|
||||
// if no transfer queue, return selected files
|
||||
match self.get_selected_index(id) {
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::fs::File as StdFile;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use remotefs::fs::Metadata;
|
||||
|
||||
use super::{File, FileTransferActivity, LogLevel};
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_newfile(&mut self, input: String) {
|
||||
// Check if file exists
|
||||
let mut file_exists: bool = false;
|
||||
for file in self.host_bridge().iter_files_all() {
|
||||
if input == file.name() {
|
||||
file_exists = true;
|
||||
}
|
||||
}
|
||||
if file_exists {
|
||||
self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",));
|
||||
/// Create a new empty file via the active tab's pane.
|
||||
pub(crate) fn action_newfile(&mut self, input: String) {
|
||||
// Check if file exists in current explorer listing
|
||||
if self
|
||||
.browser
|
||||
.explorer()
|
||||
.iter_files_all()
|
||||
.any(|file| input == file.name())
|
||||
{
|
||||
self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create file
|
||||
let file_path: PathBuf = PathBuf::from(input.as_str());
|
||||
let file_path = PathBuf::from(input.as_str());
|
||||
let writer = match self
|
||||
.host_bridge
|
||||
.browser
|
||||
.fs_pane_mut()
|
||||
.fs
|
||||
.create_file(file_path.as_path(), &Metadata::default())
|
||||
{
|
||||
Ok(f) => f,
|
||||
@@ -40,7 +40,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
};
|
||||
// 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(
|
||||
LogLevel::Error,
|
||||
format!("Could not write file \"{}\": {}", file_path.display(), err),
|
||||
@@ -53,67 +53,4 @@ impl FileTransferActivity {
|
||||
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
|
||||
//!
|
||||
//! `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
|
||||
// ext
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||
use super::{File, FileTransferActivity, LogLevel, TransferPayload};
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Open local file
|
||||
pub(crate) fn action_open_local(&mut self) {
|
||||
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![],
|
||||
};
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_local_file(x, None));
|
||||
/// Open selected file(s) with default application
|
||||
pub(crate) fn action_open(&mut self) {
|
||||
let entries = self.get_selected_entries().get_files();
|
||||
entries.iter().for_each(|x| self.open_file(x, None));
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
// clear queue
|
||||
self.browser.explorer_mut().clear_queue();
|
||||
self.reload_browser_file_list();
|
||||
}
|
||||
|
||||
/// Open local file
|
||||
pub(crate) fn action_open_remote(&mut self) {
|
||||
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![],
|
||||
};
|
||||
entries
|
||||
.iter()
|
||||
.for_each(|x| self.action_open_remote_file(x, None));
|
||||
/// Open selected file(s) with provided application
|
||||
pub(crate) fn action_open_with(&mut self, with: &str) {
|
||||
let entries = self.get_selected_entries().get_files();
|
||||
entries.iter().for_each(|x| self.open_file(x, Some(with)));
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
self.reload_remote_filelist();
|
||||
// clear queue
|
||||
self.browser.explorer_mut().clear_queue();
|
||||
self.reload_browser_file_list();
|
||||
}
|
||||
|
||||
/// Perform open lopcal file
|
||||
pub(crate) fn action_open_local_file(&mut self, entry: &File, open_with: Option<&str>) {
|
||||
if self.host_bridge.is_localhost() {
|
||||
/// Open a file, dispatching based on whether the active pane is localhost.
|
||||
pub(crate) fn open_file(&mut self, entry: &File, open_with: Option<&str>) {
|
||||
if self.browser.fs_pane().fs.is_localhost() {
|
||||
// Direct open from local path
|
||||
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);
|
||||
} 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
|
||||
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
|
||||
let tmpfile: String =
|
||||
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>) {
|
||||
// Download file
|
||||
let tmpfile: String =
|
||||
@@ -144,7 +104,7 @@ impl FileTransferActivity {
|
||||
let tmpfile = cache.join(tmpfile);
|
||||
|
||||
// open from host bridge
|
||||
let mut reader = match self.host_bridge.open_file(entry.path()) {
|
||||
let mut reader = match self.browser.local_pane_mut().fs.open_file(entry.path()) {
|
||||
Ok(reader) => reader,
|
||||
Err(err) => {
|
||||
self.log(
|
||||
|
||||
@@ -1,62 +1,38 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use remotefs::RemoteErrorType;
|
||||
|
||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_rename(&mut self, input: String) {
|
||||
match self.get_local_selected_entries() {
|
||||
/// Rename / move the currently selected entries via the active tab's pane.
|
||||
pub(crate) fn action_rename(&mut self, input: String) {
|
||||
match self.get_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
let dest_path: PathBuf = PathBuf::from(input);
|
||||
self.local_rename_file(&entry, dest_path.as_path());
|
||||
let dest_path = PathBuf::from(input);
|
||||
self.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.local_rename_file(&entry, dest_path.as_path());
|
||||
self.rename_file(&entry, dest_path.as_path());
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
// clear selection and reload
|
||||
self.browser.explorer_mut().clear_queue();
|
||||
self.reload_browser_file_list();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_remote_rename(&mut self, input: String) {
|
||||
match self.get_remote_selected_entries() {
|
||||
SelectedFile::One(entry) => {
|
||||
let dest_path: PathBuf = PathBuf::from(input);
|
||||
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) {
|
||||
/// Rename a single file via the active tab's pane.
|
||||
/// Falls back to `tricky_move` on remote tabs when rename fails.
|
||||
fn rename_file(&mut self, entry: &File, dest: &Path) {
|
||||
match self.browser.fs_pane_mut().fs.rename(entry, dest) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
@@ -67,20 +43,31 @@ impl FileTransferActivity {
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not move \"{}\" to \"{}\": {}",
|
||||
entry.path().display(),
|
||||
dest.display(),
|
||||
err
|
||||
),
|
||||
),
|
||||
Err(err) => {
|
||||
if !self.is_local_tab() {
|
||||
// Try tricky_move as a fallback on remote
|
||||
debug!("Rename failed ({err}); attempting tricky_move");
|
||||
self.tricky_move(entry, dest);
|
||||
} else {
|
||||
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) {
|
||||
match self.client.as_mut().mov(entry.path(), dest) {
|
||||
match self.browser.remote_pane_mut().fs.rename(entry, dest) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
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);
|
||||
}
|
||||
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() {
|
||||
// Delete remote existing 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(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -42,20 +42,20 @@ enum AllOpts {
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub(crate) fn action_local_saveas(&mut self, input: String) {
|
||||
self.local_send_file(TransferOpts::default().save_as(Some(input)));
|
||||
pub(crate) fn action_saveas(&mut self, input: String) {
|
||||
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) {
|
||||
self.remote_recv_file(TransferOpts::default().save_as(Some(input)));
|
||||
}
|
||||
|
||||
pub(crate) fn action_local_send(&mut self) {
|
||||
self.local_send_file(TransferOpts::default());
|
||||
}
|
||||
|
||||
pub(crate) fn action_remote_recv(&mut self) {
|
||||
self.remote_recv_file(TransferOpts::default());
|
||||
pub(crate) fn action_transfer_file(&mut self) {
|
||||
if self.is_local_tab() {
|
||||
self.local_send_file(TransferOpts::default());
|
||||
} else {
|
||||
self.remote_recv_file(TransferOpts::default());
|
||||
}
|
||||
}
|
||||
|
||||
fn local_send_file(&mut self, opts: TransferOpts) {
|
||||
@@ -64,7 +64,7 @@ impl FileTransferActivity {
|
||||
SelectedFile::One(entry) => {
|
||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||
if self.config().get_prompt_on_file_replace()
|
||||
&& self.remote_file_exists(file_to_check.as_path())
|
||||
&& self.file_exists(file_to_check.as_path(), false)
|
||||
&& !self
|
||||
.should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name()))
|
||||
{
|
||||
@@ -124,7 +124,7 @@ impl FileTransferActivity {
|
||||
SelectedFile::One(entry) => {
|
||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||
if self.config().get_prompt_on_file_replace()
|
||||
&& self.host_bridge_file_exists(file_to_check.as_path())
|
||||
&& self.file_exists(file_to_check.as_path(), true)
|
||||
&& !self
|
||||
.should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name()))
|
||||
{
|
||||
@@ -307,8 +307,8 @@ impl FileTransferActivity {
|
||||
files.into_iter().partition(|(x, dest_path)| {
|
||||
let p = Self::file_to_check_many(x, dest_path);
|
||||
match file_exists {
|
||||
CheckFileExists::Remote => self.remote_file_exists(p.as_path()),
|
||||
CheckFileExists::HostBridge => self.host_bridge_file_exists(p.as_path()),
|
||||
CheckFileExists::Remote => self.file_exists(p.as_path(), false),
|
||||
CheckFileExists::HostBridge => self.file_exists(p.as_path(), true),
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
use std::path::Path;
|
||||
|
||||
use super::{File, FileTransferActivity};
|
||||
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// List directory contents via the active tab's pane.
|
||||
pub(crate) fn action_scan(&mut self, p: &Path) -> Result<Vec<File>, String> {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => self
|
||||
.host_bridge
|
||||
.list_dir(p)
|
||||
.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)),
|
||||
}
|
||||
self.browser
|
||||
.fs_pane_mut()
|
||||
.fs
|
||||
.list_dir(p)
|
||||
.map_err(|e| format!("Failed to list directory: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use super::{File, FileTransferActivity};
|
||||
@@ -11,16 +11,15 @@ enum SubmitAction {
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Decides which action to perform on submit for local explorer
|
||||
/// Return true whether the directory changed
|
||||
pub(crate) fn action_submit_local(&mut self, entry: File) {
|
||||
/// Decides which action to perform on submit, dispatching via the active tab's pane.
|
||||
pub(crate) fn action_submit(&mut self, entry: File) {
|
||||
let (action, entry) = if entry.is_dir() {
|
||||
(SubmitAction::ChangeDir, entry)
|
||||
} 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 stat_file = match self.host_bridge.stat(symlink.as_path()) {
|
||||
Ok(e) => e,
|
||||
match self.browser.fs_pane_mut().fs.stat(symlink.as_path()) {
|
||||
Ok(e) => (SubmitAction::ChangeDir, e),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Could not stat file pointed by {} ({}): {}",
|
||||
@@ -28,44 +27,14 @@ impl FileTransferActivity {
|
||||
symlink.display(),
|
||||
err
|
||||
);
|
||||
entry
|
||||
(SubmitAction::ChangeDir, entry)
|
||||
}
|
||||
};
|
||||
(SubmitAction::ChangeDir, stat_file)
|
||||
}
|
||||
} else {
|
||||
(SubmitAction::None, entry)
|
||||
};
|
||||
if let (SubmitAction::ChangeDir, entry) = (action, entry) {
|
||||
self.action_enter_local_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)
|
||||
self.action_enter_dir(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::path::PathBuf;
|
||||
@@ -8,57 +8,39 @@ use std::path::PathBuf;
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Create symlink on localhost
|
||||
pub(crate) fn action_local_symlink(&mut self, name: String) {
|
||||
if let Some(entry) = self.get_local_selected_file() {
|
||||
match self
|
||||
.host_bridge
|
||||
.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: {err}"));
|
||||
}
|
||||
/// Create a symlink for the currently selected file via the active tab's pane.
|
||||
pub(crate) fn action_symlink(&mut self, name: String) {
|
||||
let entry = if let Some(e) = self.get_selected_file() {
|
||||
e
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let link_path = PathBuf::from(name.as_str());
|
||||
match self
|
||||
.browser
|
||||
.fs_pane_mut()
|
||||
.fs
|
||||
.symlink(link_path.as_path(), entry.path())
|
||||
{
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Created symlink at {}, pointing to {}",
|
||||
name,
|
||||
entry.path().display()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy file on remote
|
||||
pub(crate) fn action_remote_symlink(&mut self, name: String) {
|
||||
if let Some(entry) = self.get_remote_selected_file() {
|
||||
match self
|
||||
.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
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not create symlink pointing to {}: {}",
|
||||
entry.path().display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// locals
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -15,17 +15,22 @@ pub enum WalkdirError {
|
||||
}
|
||||
|
||||
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 pwd = self
|
||||
.host_bridge
|
||||
.browser
|
||||
.fs_pane_mut()
|
||||
.fs
|
||||
.pwd()
|
||||
.map_err(|e| WalkdirError::Error(e.to_string()))?;
|
||||
|
||||
self.walkdir(&mut acc, &pwd, |activity, path| {
|
||||
activity
|
||||
.host_bridge
|
||||
.browser
|
||||
.fs_pane_mut()
|
||||
.fs
|
||||
.list_dir(path)
|
||||
.map_err(|e| e.to_string())
|
||||
})?;
|
||||
@@ -33,21 +38,6 @@ impl FileTransferActivity {
|
||||
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>(
|
||||
&mut self,
|
||||
acc: &mut Vec<File>,
|
||||
|
||||
@@ -6,7 +6,7 @@ use tui_realm_stdlib::Phantom;
|
||||
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::{Component, MockComponent, NoUserEvent};
|
||||
|
||||
use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
|
||||
use super::{Msg, TransferMsg, UiMsg};
|
||||
|
||||
// -- export
|
||||
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::{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)]
|
||||
pub enum Item {
|
||||
|
||||
93
src/ui/activities/filetransfer/components/popups/copy.rs
Normal file
93
src/ui/activities/filetransfer/components/popups/copy.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/ui/activities/filetransfer/components/popups/delete.rs
Normal file
72
src/ui/activities/filetransfer/components/popups/delete.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/ui/activities/filetransfer/components/popups/error.rs
Normal file
74
src/ui/activities/filetransfer/components/popups/error.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/ui/activities/filetransfer/components/popups/file_info.rs
Normal file
122
src/ui/activities/filetransfer/components/popups/file_info.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/ui/activities/filetransfer/components/popups/filter.rs
Normal file
94
src/ui/activities/filetransfer/components/popups/filter.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
204
src/ui/activities/filetransfer/components/popups/keybindings.rs
Normal file
204
src/ui/activities/filetransfer/components/popups/keybindings.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
164
src/ui/activities/filetransfer/components/popups/mkdir.rs
Normal file
164
src/ui/activities/filetransfer/components/popups/mkdir.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/ui/activities/filetransfer/components/popups/newfile.rs
Normal file
91
src/ui/activities/filetransfer/components/popups/newfile.rs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/ui/activities/filetransfer/components/popups/quit.rs
Normal file
71
src/ui/activities/filetransfer/components/popups/quit.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/ui/activities/filetransfer/components/popups/rename.rs
Normal file
93
src/ui/activities/filetransfer/components/popups/rename.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/ui/activities/filetransfer/components/popups/replace.rs
Normal file
83
src/ui/activities/filetransfer/components/popups/replace.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/ui/activities/filetransfer/components/popups/save_as.rs
Normal file
93
src/ui/activities/filetransfer/components/popups/save_as.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/ui/activities/filetransfer/components/popups/sorting.rs
Normal file
65
src/ui/activities/filetransfer/components/popups/sorting.rs
Normal file
@@ -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",
|
||||
}
|
||||
}
|
||||
96
src/ui/activities/filetransfer/components/popups/symlink.rs
Normal file
96
src/ui/activities/filetransfer/components/popups/symlink.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/ui/activities/filetransfer/components/popups/wait.rs
Normal file
75
src/ui/activities/filetransfer/components/popups/wait.rs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
162
src/ui/activities/filetransfer/components/popups/watcher.rs
Normal file
162
src/ui/activities/filetransfer/components/popups/watcher.rs
Normal file
@@ -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 {
|
||||
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) {
|
||||
if attr == Attribute::Text {
|
||||
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 {
|
||||
match cmd {
|
||||
Cmd::Type(s) => {
|
||||
@@ -234,56 +286,4 @@ impl MockComponent for TerminalComponent {
|
||||
_ => 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()
|
||||
);
|
||||
// stat fs entry
|
||||
let origin = match self.client.stat(source) {
|
||||
let origin = match self.browser.remote_pane_mut().fs.stat(source) {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
self.log(
|
||||
@@ -71,7 +71,18 @@ impl FileTransferActivity {
|
||||
}
|
||||
|
||||
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(()) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
@@ -89,7 +100,7 @@ impl FileTransferActivity {
|
||||
|
||||
fn upload_watched_file(&mut self, host: &Path, remote: &Path) {
|
||||
// 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,
|
||||
Err(err) => {
|
||||
self.log(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! ## 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 nucleo::Utf32String;
|
||||
use remotefs::File;
|
||||
|
||||
use super::pane::Pane;
|
||||
use crate::explorer::builder::FileExplorerBuilder;
|
||||
use crate::explorer::{FileExplorer, FileSorting};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
@@ -31,19 +32,19 @@ pub enum FoundExplorerTab {
|
||||
|
||||
/// Browser contains the browser options
|
||||
pub struct Browser {
|
||||
host_bridge: FileExplorer, // Local File explorer state
|
||||
remote: FileExplorer, // Remote File explorer state
|
||||
found: Option<Found>, // File explorer for find result
|
||||
tab: FileExplorerTab, // Current selected tab
|
||||
local: Pane,
|
||||
remote: Pane,
|
||||
found: Option<Found>, // File explorer for find result
|
||||
tab: FileExplorerTab, // Current selected tab
|
||||
pub sync_browsing: bool,
|
||||
}
|
||||
|
||||
impl Browser {
|
||||
/// Build a new `Browser` struct
|
||||
pub fn new(cli: &ConfigClient) -> Self {
|
||||
pub fn new(local: Pane, remote: Pane) -> Self {
|
||||
Self {
|
||||
host_bridge: Self::build_local_explorer(cli),
|
||||
remote: Self::build_remote_explorer(cli),
|
||||
local,
|
||||
remote,
|
||||
found: None,
|
||||
tab: FileExplorerTab::HostBridge,
|
||||
sync_browsing: false,
|
||||
@@ -52,8 +53,8 @@ impl Browser {
|
||||
|
||||
pub fn explorer(&self) -> &FileExplorer {
|
||||
match self.tab {
|
||||
FileExplorerTab::HostBridge => &self.host_bridge,
|
||||
FileExplorerTab::Remote => &self.remote,
|
||||
FileExplorerTab::HostBridge => &self.local.explorer,
|
||||
FileExplorerTab::Remote => &self.remote.explorer,
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
self.found.as_ref().map(|x| &x.explorer).unwrap()
|
||||
}
|
||||
@@ -62,15 +63,15 @@ impl Browser {
|
||||
|
||||
pub fn other_explorer_no_found(&self) -> &FileExplorer {
|
||||
match self.tab {
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.remote,
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.host_bridge,
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.remote.explorer,
|
||||
FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.local.explorer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn explorer_mut(&mut self) -> &mut FileExplorer {
|
||||
match self.tab {
|
||||
FileExplorerTab::HostBridge => &mut self.host_bridge,
|
||||
FileExplorerTab::Remote => &mut self.remote,
|
||||
FileExplorerTab::HostBridge => &mut self.local.explorer,
|
||||
FileExplorerTab::Remote => &mut self.remote.explorer,
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
self.found.as_mut().map(|x| &mut x.explorer).unwrap()
|
||||
}
|
||||
@@ -78,19 +79,19 @@ impl Browser {
|
||||
}
|
||||
|
||||
pub fn host_bridge(&self) -> &FileExplorer {
|
||||
&self.host_bridge
|
||||
&self.local.explorer
|
||||
}
|
||||
|
||||
pub fn host_bridge_mut(&mut self) -> &mut FileExplorer {
|
||||
&mut self.host_bridge
|
||||
&mut self.local.explorer
|
||||
}
|
||||
|
||||
pub fn remote(&self) -> &FileExplorer {
|
||||
&self.remote
|
||||
&self.remote.explorer
|
||||
}
|
||||
|
||||
pub fn remote_mut(&mut self) -> &mut FileExplorer {
|
||||
&mut self.remote
|
||||
&mut self.remote.explorer
|
||||
}
|
||||
|
||||
pub fn found(&self) -> Option<&FileExplorer> {
|
||||
@@ -151,22 +152,70 @@ impl Browser {
|
||||
/// Toggle terminal for the current tab
|
||||
pub fn toggle_terminal(&mut self, terminal: bool) {
|
||||
if self.tab == FileExplorerTab::HostBridge {
|
||||
self.host_bridge.toggle_terminal(terminal);
|
||||
self.local.explorer.toggle_terminal(terminal);
|
||||
} 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
|
||||
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
|
||||
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
|
||||
pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer {
|
||||
let mut builder = Self::build_explorer(cli);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! ## 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 pane;
|
||||
pub(crate) mod transfer;
|
||||
pub(crate) mod walkdir;
|
||||
|
||||
57
src/ui/activities/filetransfer/lib/pane.rs
Normal file
57
src/ui/activities/filetransfer/lib/pane.rs
Normal file
@@ -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
|
||||
//!
|
||||
//! `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::time::Instant;
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
mod filelist;
|
||||
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 super::browser::FileExplorerTab;
|
||||
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;
|
||||
use super::FileTransferActivity;
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
285
src/ui/activities/filetransfer/misc/filelist.rs
Normal file
285
src/ui/activities/filetransfer/misc/filelist.rs
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/ui/activities/filetransfer/misc/host.rs
Normal file
118
src/ui/activities/filetransfer/misc/host.rs
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/ui/activities/filetransfer/misc/log.rs
Normal file
84
src/ui/activities/filetransfer/misc/log.rs
Normal file
@@ -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()),
|
||||
));
|
||||
}
|
||||
}
|
||||
67
src/ui/activities/filetransfer/misc/notify.rs
Normal file
67
src/ui/activities/filetransfer/misc/notify.rs
Normal file
@@ -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
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
// This module is split into files, cause it's just too big
|
||||
mod actions;
|
||||
@@ -20,10 +20,10 @@ use std::time::Duration;
|
||||
// Includes
|
||||
use chrono::{DateTime, Local};
|
||||
use lib::browser;
|
||||
use lib::browser::Browser;
|
||||
use lib::browser::{Browser, FileExplorerTab};
|
||||
use lib::pane::Pane;
|
||||
use lib::transfer::{TransferOpts, TransferStates};
|
||||
use lib::walkdir::WalkdirStates;
|
||||
use remotefs::RemoteFs;
|
||||
use session::TransferPayload;
|
||||
use tempfile::TempDir;
|
||||
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
||||
@@ -34,7 +34,7 @@ use crate::explorer::{FileExplorer, FileSorting};
|
||||
use crate::filetransfer::{
|
||||
FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder,
|
||||
};
|
||||
use crate::host::HostBridge;
|
||||
use crate::host::RemoteBridged;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::watcher::FsWatcher;
|
||||
|
||||
@@ -237,10 +237,6 @@ pub struct FileTransferActivity {
|
||||
app: Application<Id, Msg, NoUserEvent>,
|
||||
/// Whether should redraw UI
|
||||
redraw: bool,
|
||||
/// Host bridge
|
||||
host_bridge: Box<dyn HostBridge>,
|
||||
/// Remote host client
|
||||
client: Box<dyn RemoteFs>,
|
||||
/// Browser
|
||||
browser: Browser,
|
||||
/// Current log lines
|
||||
@@ -253,10 +249,6 @@ pub struct FileTransferActivity {
|
||||
cache: Option<TempDir>,
|
||||
/// Fs watcher
|
||||
fswatcher: Option<FsWatcher>,
|
||||
/// host bridge connected
|
||||
host_bridge_connected: bool,
|
||||
/// remote connected once
|
||||
remote_connected: bool,
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
@@ -272,6 +264,25 @@ impl FileTransferActivity {
|
||||
let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client)?;
|
||||
let host_bridge_connected = host_bridge.is_localhost();
|
||||
let enable_fs_watcher = host_bridge.is_localhost();
|
||||
// Build remote client, wrapped as HostBridge via RemoteBridged
|
||||
let remote_client = RemoteFsBuilder::build(
|
||||
remote_params.protocol,
|
||||
remote_params.params.clone(),
|
||||
&config_client,
|
||||
)?;
|
||||
let remote_fs: Box<dyn crate::host::HostBridge> =
|
||||
Box::new(RemoteBridged::from(remote_client));
|
||||
// Build panes
|
||||
let local_pane = Pane::new(
|
||||
Browser::build_local_explorer(&config_client),
|
||||
host_bridge_connected,
|
||||
host_bridge,
|
||||
);
|
||||
let remote_pane = Pane::new(
|
||||
Browser::build_remote_explorer(&config_client),
|
||||
false,
|
||||
remote_fs,
|
||||
);
|
||||
Ok(Self {
|
||||
exit_reason: None,
|
||||
context: None,
|
||||
@@ -281,14 +292,8 @@ impl FileTransferActivity {
|
||||
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
||||
),
|
||||
redraw: true,
|
||||
host_bridge,
|
||||
client: RemoteFsBuilder::build(
|
||||
remote_params.protocol,
|
||||
remote_params.params.clone(),
|
||||
&config_client,
|
||||
)?,
|
||||
browser: Browser::new(&config_client),
|
||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||
browser: Browser::new(local_pane, remote_pane),
|
||||
log_records: VecDeque::with_capacity(256),
|
||||
walkdir: WalkdirStates::default(),
|
||||
transfer: TransferStates::default(),
|
||||
cache: TempDir::new().ok(),
|
||||
@@ -297,11 +302,18 @@ impl FileTransferActivity {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
host_bridge_connected,
|
||||
remote_connected: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` when the active tab targets the local side
|
||||
/// (either the main host-bridge pane or a find-result rooted there).
|
||||
fn is_local_tab(&self) -> bool {
|
||||
matches!(
|
||||
self.browser.tab(),
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge
|
||||
)
|
||||
}
|
||||
|
||||
fn host_bridge(&self) -> &FileExplorer {
|
||||
self.browser.host_bridge()
|
||||
}
|
||||
@@ -433,7 +445,7 @@ impl Activity for FileTransferActivity {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Get files at current pwd
|
||||
if self.host_bridge.is_localhost() {
|
||||
if self.browser.local_pane().fs.is_localhost() {
|
||||
debug!("Reloading host bridge directory");
|
||||
self.reload_host_bridge_dir();
|
||||
}
|
||||
@@ -460,9 +472,10 @@ impl Activity for FileTransferActivity {
|
||||
return;
|
||||
}
|
||||
// Check if connected to host bridge (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||
if (!self.host_bridge.is_connected() || !self.host_bridge_connected)
|
||||
if (!self.browser.local_pane_mut().fs.is_connected()
|
||||
|| !self.browser.local_pane().connected)
|
||||
&& !self.app.mounted(&Id::FatalPopup)
|
||||
&& !self.host_bridge.is_localhost()
|
||||
&& !self.browser.local_pane().fs.is_localhost()
|
||||
{
|
||||
let host_bridge_params = self.context().host_bridge_params().unwrap();
|
||||
let ft_params = host_bridge_params.unwrap_protocol_params();
|
||||
@@ -476,9 +489,10 @@ impl Activity for FileTransferActivity {
|
||||
self.redraw = true;
|
||||
}
|
||||
// Check if connected to remote (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||
if (!self.client.is_connected() || !self.remote_connected)
|
||||
if (!self.browser.remote_pane_mut().fs.is_connected()
|
||||
|| !self.browser.remote_pane().connected)
|
||||
&& !self.app.mounted(&Id::FatalPopup)
|
||||
&& self.host_bridge.is_connected()
|
||||
&& self.browser.local_pane_mut().fs.is_connected()
|
||||
{
|
||||
let ftparams = self.context().remote_params().unwrap();
|
||||
// print params
|
||||
@@ -522,14 +536,21 @@ impl Activity for FileTransferActivity {
|
||||
if let Err(err) = self.context_mut().terminal().clear_screen() {
|
||||
error!("Failed to clear screen: {}", err);
|
||||
}
|
||||
// Disconnect client
|
||||
if self.client.is_connected() {
|
||||
let _ = self.client.disconnect();
|
||||
// Disconnect remote
|
||||
if self.browser.remote_pane_mut().fs.is_connected() {
|
||||
let _ = self.browser.remote_pane_mut().fs.disconnect();
|
||||
}
|
||||
// disconnect host bridge
|
||||
if self.host_bridge.is_connected() {
|
||||
let _ = self.host_bridge.disconnect();
|
||||
// Disconnect host bridge
|
||||
if self.browser.local_pane_mut().fs.is_connected() {
|
||||
let _ = self.browser.local_pane_mut().fs.disconnect();
|
||||
}
|
||||
self.context.take()
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a UI operation error instead of panicking.
|
||||
fn ui_result<T>(result: Result<T, impl std::fmt::Display>) {
|
||||
if let Err(err) = result {
|
||||
error!("UI operation failed: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
105
src/ui/activities/filetransfer/session/connection.rs
Normal file
105
src/ui/activities/filetransfer/session/connection.rs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
284
src/ui/activities/filetransfer/session/navigation.rs
Normal file
284
src/ui/activities/filetransfer/session/navigation.rs
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
841
src/ui/activities/filetransfer/session/transfer.rs
Normal file
841
src/ui/activities/filetransfer/session/transfer.rs
Normal file
@@ -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
|
||||
//!
|
||||
//! `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
|
||||
// externals
|
||||
@@ -12,6 +12,7 @@ use super::actions::walkdir::WalkdirError;
|
||||
use super::browser::{FileExplorerTab, FoundExplorerTab};
|
||||
use super::{
|
||||
ExitReason, FileTransferActivity, Id, MarkQueue, Msg, TransferMsg, TransferOpts, UiMsg,
|
||||
ui_result,
|
||||
};
|
||||
|
||||
impl Update<Msg> for FileTransferActivity {
|
||||
@@ -40,13 +41,12 @@ impl FileTransferActivity {
|
||||
TransferMsg::Chmod(mode) => {
|
||||
self.umount_chmod();
|
||||
self.mount_blocking_wait("Applying new file mode…");
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge
|
||||
if self.host_bridge.is_localhost() && cfg!(windows) => {}
|
||||
FileExplorerTab::HostBridge => self.action_local_chmod(mode),
|
||||
FileExplorerTab::FindHostBridge => self.action_find_local_chmod(mode),
|
||||
FileExplorerTab::Remote => self.action_remote_chmod(mode),
|
||||
FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode),
|
||||
// Skip chmod on Windows localhost
|
||||
if !(self.is_local_tab()
|
||||
&& self.browser.local_pane().fs.is_localhost()
|
||||
&& cfg!(windows))
|
||||
{
|
||||
self.action_chmod(mode);
|
||||
}
|
||||
self.umount_wait();
|
||||
self.update_browser_file_list();
|
||||
@@ -54,11 +54,7 @@ impl FileTransferActivity {
|
||||
TransferMsg::CopyFileTo(dest) => {
|
||||
self.umount_copy();
|
||||
self.mount_blocking_wait("Copying file(s)…");
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_local_copy(dest),
|
||||
FileExplorerTab::Remote => self.action_remote_copy(dest),
|
||||
_ => panic!("Found tab doesn't support COPY"),
|
||||
}
|
||||
self.action_copy(dest);
|
||||
self.umount_wait();
|
||||
// Reload files
|
||||
self.update_browser_file_list()
|
||||
@@ -66,11 +62,7 @@ impl FileTransferActivity {
|
||||
TransferMsg::CreateSymlink(name) => {
|
||||
self.umount_symlink();
|
||||
self.mount_blocking_wait("Creating symlink…");
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_local_symlink(name),
|
||||
FileExplorerTab::Remote => self.action_remote_symlink(name),
|
||||
_ => panic!("Found tab doesn't support SYMLINK"),
|
||||
}
|
||||
self.action_symlink(name);
|
||||
self.umount_wait();
|
||||
// Reload files
|
||||
self.update_browser_file_list()
|
||||
@@ -79,15 +71,14 @@ impl FileTransferActivity {
|
||||
self.umount_radio_delete();
|
||||
self.mount_blocking_wait("Removing file(s)…");
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_local_delete(),
|
||||
FileExplorerTab::Remote => self.action_remote_delete(),
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::Remote => {
|
||||
self.action_delete();
|
||||
}
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
// Get entry
|
||||
self.action_find_delete();
|
||||
// Delete entries
|
||||
// Remove deleted entries from the find-result list
|
||||
match self.app.state(&Id::ExplorerFind) {
|
||||
Ok(State::One(StateValue::Usize(idx))) => {
|
||||
// Reload entries
|
||||
self.found_mut().unwrap().del_entry(idx);
|
||||
}
|
||||
Ok(State::Vec(values)) => {
|
||||
@@ -106,31 +97,19 @@ impl FileTransferActivity {
|
||||
}
|
||||
self.umount_wait();
|
||||
// Reload files
|
||||
match self.browser.tab() {
|
||||
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(),
|
||||
}
|
||||
self.update_browser_file_list();
|
||||
}
|
||||
TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::HostBridge => {
|
||||
if let Some(entry) = self.get_local_selected_file() {
|
||||
self.action_submit_local(entry);
|
||||
TransferMsg::EnterDirectory
|
||||
if self.browser.tab() == FileExplorerTab::HostBridge
|
||||
|| self.browser.tab() == FileExplorerTab::Remote =>
|
||||
{
|
||||
if let Some(entry) = self.get_selected_file() {
|
||||
self.action_submit(entry);
|
||||
// Update file list if sync
|
||||
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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
self.update_browser_file_list();
|
||||
}
|
||||
}
|
||||
TransferMsg::EnterDirectory => {
|
||||
@@ -145,22 +124,13 @@ impl FileTransferActivity {
|
||||
self.update_browser_file_list()
|
||||
}
|
||||
TransferMsg::ExecuteCmd(cmd) => {
|
||||
// Exec command
|
||||
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"),
|
||||
};
|
||||
self.action_exec_cmd(cmd);
|
||||
}
|
||||
TransferMsg::GetFileSize => {
|
||||
self.action_get_file_size();
|
||||
}
|
||||
TransferMsg::GoTo(dir) => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_change_local_dir(dir),
|
||||
FileExplorerTab::Remote => self.action_change_remote_dir(dir),
|
||||
_ => panic!("Found tab doesn't support GOTO"),
|
||||
}
|
||||
self.action_change_dir(dir);
|
||||
// Umount
|
||||
self.umount_goto();
|
||||
// Reload files if sync
|
||||
@@ -171,56 +141,24 @@ impl FileTransferActivity {
|
||||
self.update_browser_file_list()
|
||||
}
|
||||
TransferMsg::GoToParentDirectory => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => {
|
||||
self.action_go_to_local_upper_dir();
|
||||
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.action_go_to_upper_dir();
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.update_browser_file_list_swapped();
|
||||
}
|
||||
self.update_browser_file_list();
|
||||
}
|
||||
TransferMsg::GoToPreviousDirectory => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => {
|
||||
self.action_go_to_previous_local_dir();
|
||||
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.action_go_to_previous_dir();
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.update_browser_file_list_swapped();
|
||||
}
|
||||
self.update_browser_file_list();
|
||||
}
|
||||
TransferMsg::InitFuzzySearch => {
|
||||
// Mount wait
|
||||
self.mount_walkdir_wait();
|
||||
// Find
|
||||
let res: Result<Vec<File>, WalkdirError> = match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_walkdir_local(),
|
||||
FileExplorerTab::Remote => self.action_walkdir_remote(),
|
||||
_ => panic!("Trying to search for files, while already in a find result"),
|
||||
};
|
||||
let res: Result<Vec<File>, WalkdirError> = self.action_walkdir();
|
||||
// Umount wait
|
||||
self.umount_wait();
|
||||
// Match result
|
||||
@@ -266,36 +204,28 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
TransferMsg::Mkdir(dir) => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_local_mkdir(dir),
|
||||
FileExplorerTab::Remote => self.action_remote_mkdir(dir),
|
||||
_ => {}
|
||||
}
|
||||
self.action_mkdir(dir);
|
||||
self.umount_mkdir();
|
||||
// Reload files
|
||||
self.update_browser_file_list()
|
||||
}
|
||||
TransferMsg::NewFile(name) => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_local_newfile(name),
|
||||
FileExplorerTab::Remote => self.action_remote_newfile(name),
|
||||
_ => {}
|
||||
}
|
||||
self.action_newfile(name);
|
||||
self.umount_newfile();
|
||||
// Reload files
|
||||
self.update_browser_file_list()
|
||||
}
|
||||
TransferMsg::OpenFile => match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_open_local(),
|
||||
FileExplorerTab::Remote => self.action_open_remote(),
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::Remote => self.action_open(),
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
self.action_find_open()
|
||||
}
|
||||
},
|
||||
TransferMsg::OpenFileWith(prog) => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_local_open_with(&prog),
|
||||
FileExplorerTab::Remote => self.action_remote_open_with(&prog),
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::Remote => {
|
||||
self.action_open_with(&prog)
|
||||
}
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
self.action_find_open_with(&prog)
|
||||
}
|
||||
@@ -314,11 +244,7 @@ impl FileTransferActivity {
|
||||
TransferMsg::RenameFile(dest) => {
|
||||
self.umount_rename();
|
||||
self.mount_blocking_wait("Moving file(s)…");
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_local_rename(dest),
|
||||
FileExplorerTab::Remote => self.action_remote_rename(dest),
|
||||
_ => {}
|
||||
}
|
||||
self.action_rename(dest);
|
||||
self.umount_wait();
|
||||
// Reload files
|
||||
self.update_browser_file_list()
|
||||
@@ -335,10 +261,10 @@ impl FileTransferActivity {
|
||||
TransferMsg::SaveFileAs(dest) => {
|
||||
self.umount_saveas();
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_local_saveas(dest),
|
||||
FileExplorerTab::Remote => self.action_remote_saveas(dest),
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::Remote => {
|
||||
self.action_saveas(dest)
|
||||
}
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
// Get entry
|
||||
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::TransferFile => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.action_local_send(),
|
||||
FileExplorerTab::Remote => self.action_remote_recv(),
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::Remote => {
|
||||
self.action_transfer_file()
|
||||
}
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => {
|
||||
self.action_find_transfer(TransferOpts::default())
|
||||
}
|
||||
@@ -399,13 +326,13 @@ impl FileTransferActivity {
|
||||
// Set focus
|
||||
match new_tab {
|
||||
FileExplorerTab::HostBridge => {
|
||||
assert!(self.app.active(&Id::ExplorerHostBridge).is_ok())
|
||||
ui_result(self.app.active(&Id::ExplorerHostBridge));
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
assert!(self.app.active(&Id::ExplorerRemote).is_ok())
|
||||
ui_result(self.app.active(&Id::ExplorerRemote));
|
||||
}
|
||||
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);
|
||||
@@ -476,10 +403,10 @@ impl FileTransferActivity {
|
||||
self.update_find_list();
|
||||
}
|
||||
UiMsg::GoToTransferQueue => {
|
||||
assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok());
|
||||
ui_result(self.app.active(&Id::TransferQueueHostBridge));
|
||||
}
|
||||
UiMsg::LogBackTabbed => {
|
||||
assert!(self.app.active(&Id::ExplorerHostBridge).is_ok());
|
||||
ui_result(self.app.active(&Id::ExplorerHostBridge));
|
||||
}
|
||||
UiMsg::MarkFile(index) => {
|
||||
self.action_mark_file(index);
|
||||
@@ -507,18 +434,16 @@ impl FileTransferActivity {
|
||||
self.umount_quit();
|
||||
}
|
||||
UiMsg::ShowChmodPopup => {
|
||||
let selected_file = match self.browser.tab() {
|
||||
#[cfg(posix)]
|
||||
FileExplorerTab::HostBridge => self.get_local_selected_entries(),
|
||||
#[cfg(posix)]
|
||||
FileExplorerTab::FindHostBridge => self.get_found_selected_entries(),
|
||||
FileExplorerTab::Remote => self.get_remote_selected_entries(),
|
||||
FileExplorerTab::FindRemote => self.get_found_selected_entries(),
|
||||
#[cfg(win)]
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => {
|
||||
SelectedFile::None
|
||||
}
|
||||
// On Windows localhost, chmod is not supported
|
||||
#[cfg(win)]
|
||||
let selected_file = if self.is_local_tab() {
|
||||
SelectedFile::None
|
||||
} else {
|
||||
self.get_selected_entries()
|
||||
};
|
||||
#[cfg(posix)]
|
||||
let selected_file = self.get_selected_entries();
|
||||
|
||||
if let Some(mode) = selected_file.unix_pex() {
|
||||
self.mount_chmod(
|
||||
mode,
|
||||
@@ -541,18 +466,8 @@ impl FileTransferActivity {
|
||||
self.browser.toggle_terminal(true);
|
||||
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 => {
|
||||
if let SelectedFile::One(file) = self.get_found_selected_entries() {
|
||||
if let SelectedFile::One(file) = self.get_selected_entries() {
|
||||
self.mount_file_info(&file);
|
||||
}
|
||||
}
|
||||
@@ -567,12 +482,12 @@ impl FileTransferActivity {
|
||||
UiMsg::ShowRenamePopup => self.mount_rename(),
|
||||
UiMsg::ShowSaveAsPopup => self.mount_saveas(),
|
||||
UiMsg::ShowSymlinkPopup => {
|
||||
if match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge => self.is_local_selected_one(),
|
||||
FileExplorerTab::Remote => self.is_remote_selected_one(),
|
||||
// Symlink is not available from find-result tabs
|
||||
let can_symlink = match self.browser.tab() {
|
||||
FileExplorerTab::HostBridge | FileExplorerTab::Remote => self.is_selected_one(),
|
||||
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => false,
|
||||
} {
|
||||
// Only if only one entry is selected
|
||||
};
|
||||
if can_symlink {
|
||||
self.mount_symlink();
|
||||
} else {
|
||||
self.mount_error(
|
||||
@@ -604,25 +519,25 @@ impl FileTransferActivity {
|
||||
|
||||
UiMsg::BottomPanelLeft => match self.app.focus() {
|
||||
Some(Id::TransferQueueHostBridge) => {
|
||||
assert!(self.app.active(&Id::Log).is_ok())
|
||||
ui_result(self.app.active(&Id::Log));
|
||||
}
|
||||
Some(Id::TransferQueueRemote) => {
|
||||
assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok())
|
||||
ui_result(self.app.active(&Id::TransferQueueHostBridge));
|
||||
}
|
||||
Some(Id::Log) => {
|
||||
assert!(self.app.active(&Id::TransferQueueRemote).is_ok())
|
||||
ui_result(self.app.active(&Id::TransferQueueRemote));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
UiMsg::BottomPanelRight => match self.app.focus() {
|
||||
Some(Id::TransferQueueHostBridge) => {
|
||||
assert!(self.app.active(&Id::TransferQueueRemote).is_ok())
|
||||
ui_result(self.app.active(&Id::TransferQueueRemote));
|
||||
}
|
||||
Some(Id::TransferQueueRemote) => {
|
||||
assert!(self.app.active(&Id::Log).is_ok())
|
||||
ui_result(self.app.active(&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
388
src/ui/activities/filetransfer/view/layout.rs
Normal file
388
src/ui/activities/filetransfer/view/layout.rs
Normal file
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
602
src/ui/activities/filetransfer/view/popups.rs
Normal file
602
src/ui/activities/filetransfer/view/popups.rs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
81
src/ui/activities/filetransfer/view/status.rs
Normal file
81
src/ui/activities/filetransfer/view/status.rs
Normal file
@@ -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![],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -338,9 +338,9 @@ fn parse_kube_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
||||
fn parse_smb_remote_opts(s: &str) -> Result<FileTransferParams, String> {
|
||||
match REMOTE_SMB_OPT_REGEX.captures(s) {
|
||||
Some(groups) => {
|
||||
let username: Option<String> = match groups.get(1) {
|
||||
let username = match groups.get(1) {
|
||||
Some(group) => Some(group.as_str().to_string()),
|
||||
None => Some(whoami::username()),
|
||||
None => whoami::username().ok(),
|
||||
};
|
||||
let address = match groups.get(2) {
|
||||
Some(group) => group.as_str().to_string(),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Ext
|
||||
|
||||
use rand::distr::Alphanumeric;
|
||||
use rand::{Rng, rng};
|
||||
use rand::{RngExt as _, rng};
|
||||
|
||||
/// Generate a random alphanumeric string with provided length
|
||||
pub fn random_alphanumeric_with_len(len: usize) -> String {
|
||||
|
||||
Reference in New Issue
Block a user