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:
Christian Visintin
2026-02-27 20:58:31 +00:00
committed by GitHub
parent 3fb61a76fe
commit a252caa66b
75 changed files with 6595 additions and 6704 deletions

4
.gitignore vendored
View File

@@ -24,3 +24,7 @@ dist/pkgs/arch/*.tar.gz
dist/pkgs/
dist/build/macos/openssl/
.idea/
.claude/

109
CLAUDE.md Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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"

View File

@@ -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!(

View File

@@ -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())
}

View File

@@ -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.

View File

@@ -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))]

View File

@@ -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-Супер-Секрет";

View File

@@ -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())),
}
}
}

View File

@@ -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!(

View File

@@ -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,

View File

@@ -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,

View File

@@ -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!(

View File

@@ -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());
}
}
}

View File

@@ -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(),
),
);

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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}"),
),
}
}
}

View File

@@ -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) {

View File

@@ -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()),
);
}
}
}
}
}
}
}

View File

@@ -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(

View File

@@ -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!(

View File

@@ -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),
}
});

View File

@@ -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))
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
),
);
}
}
}

View File

@@ -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>,

View 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

View File

@@ -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 {

View 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,
}
}
}

View 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,
}
}
}

View 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 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,
}
}
}

View 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,
}
}
}

View 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,
}
}
}

View 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,
}
}
}

View 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,
}
}
}

View 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,
}
}
}

View 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,
}
}
}

View 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 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,
}
}
}

View File

@@ -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
}
}
}

View 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,
}
}
}

View 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,
}
}
}

View 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,
}
}
}

View 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,
}
}
}

View 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)
}
}
}

View File

@@ -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",
}
}

View 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,
}
}
}

View 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
}
}
}

View 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,
}
}
}

View File

@@ -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);
}
}

View File

@@ -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(

View File

@@ -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);

View File

@@ -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;

View 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()));
}
}

View File

@@ -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;

View File

@@ -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()
}
}
}
}

View 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()
}
}
}
}

View 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)
}
}
}
}

View 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()),
));
}
}

View 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
)
}
}
}
}

View File

@@ -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

View 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);
}
}

View 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()
}
}

View 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(())
}
}

View File

@@ -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

View 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
)
}
}

View 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);
}
}

View 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![],
));
}
}

View File

@@ -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(),

View File

@@ -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 {