//! ## FileTransferActivity //! //! `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; mod components; mod fswatcher; mod lib; mod misc; mod session; mod update; mod view; // locals use std::collections::VecDeque; use std::path::PathBuf; use std::time::Duration; // Includes use chrono::{DateTime, Local}; use lib::browser; use lib::browser::{Browser, FileExplorerTab}; use lib::pane::Pane; use lib::transfer::{TransferOpts, TransferStates}; use lib::walkdir::WalkdirStates; use session::TransferPayload; use tempfile::TempDir; use tuirealm::application::Application; use tuirealm::event::NoUserEvent; use tuirealm::listener::EventListenerCfg; use tuirealm::terminal::TerminalAdapter; use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason}; use crate::config::themes::Theme; use crate::explorer::{FileExplorer, FileSorting}; use crate::filetransfer::{ FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder, }; use crate::host::RemoteBridged; use crate::system::config_client::ConfigClient; use crate::system::watcher::FsWatcher; // -- components #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum MarkQueue { Local, Remote, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] enum Id { ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExplorerFind, ExplorerHostBridge, ExplorerRemote, FatalPopup, FileInfoPopup, FilterPopup, FooterBar, GlobalListener, GotoPopup, KeybindingsPopup, Log, MkdirPopup, NewfilePopup, OpenWithPopup, TransferProgressBarFull, TransferProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup, SaveAsPopup, SortingPopup, StatusBarHostBridge, StatusBarRemote, SymlinkPopup, SyncBrowsingMkdirPopup, TerminalHostBridge, TerminalRemote, TransferQueueHostBridge, TransferQueueRemote, WaitPopup, WatchedPathsList, WatcherPopup, } #[derive(Debug, PartialEq)] enum Msg { PendingAction(PendingActionMsg), Transfer(TransferMsg), Ui(UiMsg), None, } #[derive(Debug, PartialEq)] enum PendingActionMsg { CloseSyncBrowsingMkdirPopup, MakePendingDirectory, /// Replace file popup ReplaceCancel, ReplaceOverwrite, ReplaceOverwriteAll, ReplaceSkip, ReplaceSkipAll, } #[derive(Debug, PartialEq)] enum TransferMsg { AbortWalkdir, AbortTransfer, Chmod(remotefs::fs::UnixPex), CopyFileTo(String), CreateSymlink(String), DeleteFile, EnterDirectory, ExecuteCmd(String), GetFileSize, GoTo(String), GoToParentDirectory, GoToPreviousDirectory, InitFuzzySearch, Mkdir(String), NewFile(String), OpenFile, OpenFileWith(String), OpenTextFile, ReloadDir, RenameFile(String), RescanGotoFiles(PathBuf), SaveFileAs(String), ToggleWatch, ToggleWatchFor(usize), TransferFile, } #[derive(Debug, PartialEq)] enum UiMsg { BottomPanelLeft, BottomPanelRight, ChangeFileSorting(FileSorting), ChangeTransferWindow, CloseChmodPopup, CloseCopyPopup, CloseDeletePopup, CloseDisconnectPopup, CloseErrorPopup, CloseExecPopup, CloseFatalPopup, CloseFileInfoPopup, CloseFileSortingPopup, CloseFilterPopup, CloseFindExplorer, CloseGotoPopup, CloseKeybindingsPopup, CloseMkdirPopup, CloseNewFilePopup, CloseOpenWithPopup, CloseQuitPopup, CloseRenamePopup, CloseSaveAsPopup, CloseSymlinkPopup, CloseWatchedPathsList, CloseWatcherPopup, Disconnect, FilterFiles(String), FuzzySearch(String), LogBackTabbed, /// Mark file on the list; usize is the index of the file MarkFile(usize), MarkRemove(MarkQueue, PathBuf), /// Mark all file at tab MarkAll, /// Clear all marks MarkClear, Quit, ShowChmodPopup, ShowCopyPopup, ShowDeletePopup, ShowDisconnectPopup, ShowTerminal, ShowFileInfoPopup, ShowFileSortingPopup, ShowFilterPopup, ShowGotoPopup, ShowKeybindingsPopup, GoToTransferQueue, ShowMkdirPopup, ShowNewFilePopup, ShowOpenWithPopup, ShowQuitPopup, ShowRenamePopup, ShowSaveAsPopup, ShowSymlinkPopup, ShowWatchedPathsList, ShowWatcherPopup, ToggleHiddenFiles, ToggleSyncBrowsing, WindowResized, } /// Log level type enum LogLevel { Error, Warn, Info, } /// Log record entry struct LogRecord { pub time: DateTime, pub level: LogLevel, pub msg: String, } impl LogRecord { /// Instantiates a new LogRecord pub fn new(level: LogLevel, msg: String) -> LogRecord { LogRecord { time: Local::now(), level, msg, } } } /// FileTransferActivity is the data holder for the file transfer activity pub struct FileTransferActivity { /// Exit reason exit_reason: Option, /// Context holder context: Option, /// Tui-realm application app: Application, /// Whether should redraw UI redraw: bool, /// Browser browser: Browser, /// Current log lines log_records: VecDeque, /// Fuzzy search states walkdir: WalkdirStates, /// Transfer states transfer: TransferStates, /// Temporary directory where to store temporary stuff cache: Option, /// Fs watcher fswatcher: Option, } impl FileTransferActivity { /// Instantiates a new FileTransferActivity pub fn new( host_bridge_params: HostBridgeParams, remote_params: &FileTransferParams, ticks: Duration, ) -> Result { // Get config client let config_client: ConfigClient = Self::init_config_client(); // init host bridge 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 = Box::new(RemoteBridged::from(remote_client)); // Build panes let local_pane = Pane::new( Browser::build_local_explorer(&config_client), host_bridge_connected, host_bridge, ); let remote_pane = Pane::new( Browser::build_remote_explorer(&config_client), false, remote_fs, ); Ok(Self { exit_reason: None, context: None, app: Application::init( EventListenerCfg::default().crossterm_input_listener(ticks, CROSSTERM_MAX_POLL), ), redraw: true, browser: Browser::new(local_pane, remote_pane), log_records: VecDeque::with_capacity(256), walkdir: WalkdirStates::default(), transfer: TransferStates::default(), cache: TempDir::new().ok(), fswatcher: if enable_fs_watcher { FsWatcher::init(Duration::from_secs(5)).ok() } else { None }, }) } /// 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() } fn host_bridge_mut(&mut self) -> &mut FileExplorer { self.browser.host_bridge_mut() } fn remote(&self) -> &FileExplorer { self.browser.remote() } fn remote_mut(&mut self) -> &mut FileExplorer { self.browser.remote_mut() } fn found(&self) -> Option<&FileExplorer> { self.browser.found() } fn found_mut(&mut self) -> Option<&mut FileExplorer> { self.browser.found_mut() } /// Enqueue a file to be transferred fn enqueue_file(&mut self, index: usize) { let Some(src) = self .browser .explorer() .get(index) .map(|item| item.path().to_path_buf()) else { return; }; if self.browser.explorer().enqueued().contains_key(&src) { debug!("File already marked, unmarking {}", src.display()); self.browser.explorer_mut().dequeue(&src); } else { debug!("Marking file {}", src.display()); // Destination is the other pane's working directory joined with the file // name, so the queued entry holds the full target path (not just the dir). let mut dest = self.browser.other_explorer_no_found().wrkdir.clone(); if let Some(name) = src.file_name() { dest.push(name); } self.browser.explorer_mut().enqueue(&src, &dest); } self.reload_browser_file_list(); self.refresh_host_bridge_transfer_queue(); self.refresh_remote_transfer_queue(); } fn enqueue_all(&mut self) { let dest = self.browser.other_explorer_no_found().wrkdir.clone(); self.browser.explorer_mut().enqueue_all(&dest); self.reload_browser_file_list(); self.refresh_host_bridge_transfer_queue(); self.refresh_remote_transfer_queue(); } fn clear_queue(&mut self) { self.browser.explorer_mut().clear_queue(); self.reload_browser_file_list(); self.refresh_host_bridge_transfer_queue(); self.refresh_remote_transfer_queue(); } /// Get file name for a file in cache fn get_cache_tmp_name(&self, name: &str, file_type: Option<&str>) -> Option { self.cache.as_ref().map(|_| { let base: String = format!( "{}-{}", name, std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() ); match file_type { None => base, Some(file_type) => format!("{base}.{file_type}"), } }) } /// Returns a reference to context fn context(&self) -> &Context { self.context.as_ref().unwrap() } /// Returns a mutable reference to context fn context_mut(&mut self) -> &mut Context { self.context.as_mut().unwrap() } /// Returns config client reference fn config(&self) -> &ConfigClient { self.context().config() } /// Get a reference to `Theme` fn theme(&self) -> &Theme { self.context().theme_provider().theme() } /// Map a function to fs watcher if any fn map_on_fswatcher(&mut self, mapper: F) -> Option where F: FnOnce(&mut FsWatcher) -> T, { self.fswatcher.as_mut().map(mapper) } } /** * Activity Trait * Keep it clean :) * Use methods instead! */ impl Activity for FileTransferActivity { /// `on_create` is the function which must be called to initialize the activity. /// `on_create` must initialize all the data structures used by the activity fn on_create(&mut self, context: Context) { debug!("Initializing activity..."); // Set context self.context = Some(context); // Clear terminal if let Err(err) = self.context.as_mut().unwrap().terminal().clear_screen() { error!("Failed to clear screen: {}", err); } // Put raw mode on enabled if let Err(err) = self.context_mut().terminal().enable_raw_mode() { error!("Failed to enter raw mode: {}", err); } // Get files at current pwd if self.browser.local_pane().fs.is_localhost() { debug!("Reloading host bridge directory"); self.reload_host_bridge_dir(); } debug!("Read working directory"); // Configure text editor self.setup_text_editor(); debug!("Setup text editor"); // init view self.init(); debug!("Initialized view"); // Verify error state from context if let Some(err) = self.context.as_mut().unwrap().error() { error!("Fatal error on create: {}", err); self.mount_fatal(&err); } info!("Created FileTransferActivity"); } /// `on_draw` is the function which draws the graphical interface. /// This function must be called at each tick to refresh the interface fn on_draw(&mut self) { // Context must be something if self.context.is_none() { return; } // Check if connected to host bridge (popup must be None, otherwise would try reconnecting in loop in case of error) if (!self.browser.local_pane_mut().fs.is_connected() || !self.browser.local_pane().connected) && !self.app.mounted(&Id::FatalPopup) && !self.browser.local_pane().fs.is_localhost() { let host_bridge_params = self.context().host_bridge_params().unwrap(); let msg: String = match host_bridge_params.protocol_params() { Some(ft_params) => Self::get_connection_msg(ft_params), None => String::from("Connecting..."), }; // Set init state to connecting popup self.mount_blocking_wait(msg.as_str()); // Connect to remote self.connect_to_host_bridge(); // Redraw self.redraw = true; } // Check if connected to remote (popup must be None, otherwise would try reconnecting in loop in case of error) if (!self.browser.remote_pane_mut().fs.is_connected() || !self.browser.remote_pane().connected) && !self.app.mounted(&Id::FatalPopup) && self.browser.local_pane_mut().fs.is_connected() { let ftparams = self.context().remote_params().unwrap(); // print params let msg: String = Self::get_connection_msg(&ftparams.params); // Set init state to connecting popup self.mount_blocking_wait(msg.as_str()); // Connect to remote self.connect_to_remote(); // Redraw self.redraw = true; } self.tick(); // poll self.poll_watcher(); // View if self.redraw { self.view(); } } /// `will_umount` is the method which must be able to report to the activity manager, whether /// the activity should be terminated or not. /// If not, the call will return `None`, otherwise return`Some(ExitReason)` fn will_umount(&self) -> Option<&ExitReason> { self.exit_reason.as_ref() } /// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity. /// This function must be called once before terminating the activity. fn on_destroy(&mut self) -> Option { // Destroy cache if let Some(cache) = self.cache.take() && let Err(err) = cache.close() { error!("Failed to delete cache: {}", err); } // Disable raw mode if let Err(err) = self.context_mut().terminal().disable_raw_mode() { error!("Failed to disable raw mode: {}", err); } if let Err(err) = self.context_mut().terminal().clear_screen() { error!("Failed to clear screen: {}", err); } // Disconnect remote if self.browser.remote_pane_mut().fs.is_connected() { let _ = self.browser.remote_pane_mut().fs.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(result: Result) { if let Err(err) = result { error!("UI operation failed: {err}"); } }