Files
termscp/src/ui/activities/filetransfer/actions/mod.rs
Christian Visintin f6d35f1eea 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.
2026-04-19 01:54:46 +05:30

210 lines
6.6 KiB
Rust

//! ## FileTransferActivity
//!
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
use std::path::{Path, PathBuf};
use remotefs::File;
use remotefs::fs::UnixPex;
use tuirealm::{State, StateValue};
use super::browser::FileExplorerTab;
use super::lib::browser::FoundExplorerTab;
use super::{
FileTransferActivity, Id, LogLevel, Msg, PendingActionMsg, TransferMsg, TransferOpts,
TransferPayload, UiMsg,
};
use crate::explorer::FileExplorer;
// actions
pub(crate) mod change_dir;
pub(crate) mod chmod;
pub(crate) mod copy;
pub(crate) mod delete;
pub(crate) mod edit;
pub(crate) mod exec;
pub(crate) mod file_size;
pub(crate) mod filter;
pub(crate) mod find;
pub(crate) mod mark;
pub(crate) mod mkdir;
pub(crate) mod newfile;
pub(crate) mod open;
mod pending;
pub(crate) mod rename;
pub(crate) mod save;
pub(crate) mod scan;
pub(crate) mod submit;
pub(crate) mod symlink;
pub(crate) mod walkdir;
pub(crate) mod watcher;
#[derive(Debug)]
pub(crate) enum SelectedFile {
One(File),
/// List of file with their destination path
Many(Vec<(File, PathBuf)>),
None,
}
impl SelectedFile {
/// Get file mode for `SelectedFile`
/// In case is `Many` the first item mode is returned
pub fn unix_pex(&self) -> Option<UnixPex> {
match self {
Self::Many(files) => files
.iter()
.next()
.and_then(|(file, _)| file.metadata().mode),
Self::One(file) => file.metadata().mode,
Self::None => None,
}
}
/// Get files as vec
pub fn get_files(self) -> Vec<File> {
match self {
Self::One(file) => vec![file],
Self::Many(files) => files.into_iter().map(|(f, _)| f).collect(),
Self::None => vec![],
}
}
}
#[derive(Debug)]
enum SelectedFileIndex {
One(usize),
None,
}
impl From<Option<&File>> for SelectedFile {
fn from(opt: Option<&File>) -> Self {
match opt {
Some(e) => SelectedFile::One(e.clone()),
None => SelectedFile::None,
}
}
}
impl FileTransferActivity {
/// Get local file entry
pub(crate) fn get_local_selected_entries(&mut self) -> SelectedFile {
self.get_selected_files(&Id::ExplorerHostBridge)
}
/// Get remote file entry
pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile {
self.get_selected_files(&Id::ExplorerRemote)
}
/// 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_by_id(&Id::ExplorerFind)
}
/// Get selected entries from whichever tab is currently active.
pub(crate) fn get_selected_entries(&mut self) -> SelectedFile {
let id = match self.browser.tab() {
FileExplorerTab::HostBridge => Id::ExplorerHostBridge,
FileExplorerTab::Remote => Id::ExplorerRemote,
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => Id::ExplorerFind,
};
self.get_selected_files(&id)
}
/// Get single selected file from whichever tab is currently active.
pub(crate) fn get_selected_file(&self) -> Option<File> {
let id = match self.browser.tab() {
FileExplorerTab::HostBridge => Id::ExplorerHostBridge,
FileExplorerTab::Remote => Id::ExplorerRemote,
FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => Id::ExplorerFind,
};
self.get_selected_file_by_id(&id)
}
/// Returns whether only one entry is selected on the current tab.
pub(crate) fn is_selected_one(&mut self) -> bool {
matches!(self.get_selected_entries(), SelectedFile::One(_))
}
// -- private
fn get_selected_index(&self, id: &Id) -> SelectedFileIndex {
match self.app.state(id) {
Ok(State::One(StateValue::Usize(idx))) => SelectedFileIndex::One(idx),
_ => SelectedFileIndex::None,
}
}
fn get_selected_files(&mut self, id: &Id) -> SelectedFile {
let browser = self.browser_by_id(id);
// if transfer queue is not empty, return that
let transfer_queue = browser.enqueued().clone();
if !transfer_queue.is_empty() {
return SelectedFile::Many(
transfer_queue
.iter()
.filter_map(|(src, dest)| {
let src_file = self.get_file_from_path(id, src)?;
Some((src_file, dest.clone()))
})
.collect(),
);
}
let browser = self.browser_by_id(id);
// if no transfer queue, return selected files
match self.get_selected_index(id) {
SelectedFileIndex::One(idx) => {
let Some(f) = browser.get(idx) else {
return SelectedFile::None;
};
SelectedFile::One(f.clone())
}
SelectedFileIndex::None => SelectedFile::None,
}
}
fn get_file_from_path(&mut self, id: &Id, path: &Path) -> Option<File> {
match *id {
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.browser.local_pane_mut().fs.stat(path).ok(),
FoundExplorerTab::Remote => self.browser.remote_pane_mut().fs.stat(path).ok(),
}
}
_ => None,
}
}
fn browser_by_id(&self, id: &Id) -> &FileExplorer {
match *id {
Id::ExplorerHostBridge => self.host_bridge(),
Id::ExplorerRemote => self.remote(),
Id::ExplorerFind => self.found().as_ref().unwrap(),
_ => {
error!(
"browser_by_id called with unexpected id: {id:?}; falling back to local explorer"
);
self.host_bridge()
}
}
}
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) {
SelectedFileIndex::One(idx) => browser.get(idx).cloned(),
SelectedFileIndex::None => None,
}
}
}