diff --git a/src/explorer/mod.rs b/src/explorer/mod.rs index b191c84..3e6ec04 100644 --- a/src/explorer/mod.rs +++ b/src/explorer/mod.rs @@ -7,7 +7,7 @@ pub(crate) mod builder; mod formatter; // Locals use std::cmp::Reverse; -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -42,14 +42,24 @@ pub enum GroupDirs { /// File explorer states pub struct FileExplorer { - pub wrkdir: PathBuf, // Current directory - pub(crate) dirstack: VecDeque, // Stack of visited directory (max 16) - pub(crate) stack_size: usize, // Directory stack size - pub(crate) file_sorting: FileSorting, // File sorting criteria - pub(crate) group_dirs: Option, // If Some, defines how to group directories - pub(crate) opts: ExplorerOpts, // Explorer options - pub(crate) fmt: Formatter, // File formatter - files: Vec, // Files in directory + /// Current working directory + pub wrkdir: PathBuf, + /// Stack of visited directories + pub(crate) dirstack: VecDeque, + /// Stack size + pub(crate) stack_size: usize, + /// Criteria to sort file + pub(crate) file_sorting: FileSorting, + /// defines how to group directories in the explorer + pub(crate) group_dirs: Option, + /// Explorer options + pub(crate) opts: ExplorerOpts, + /// Formatter for file entries + pub(crate) fmt: Formatter, + /// Files in directory + files: Vec, + /// files enqueued for transfer. Map between source and destination + transfer_queue: HashMap, // transfer queue } impl Default for FileExplorer { @@ -63,6 +73,7 @@ impl Default for FileExplorer { opts: ExplorerOpts::empty(), fmt: Formatter::default(), files: Vec::new(), + transfer_queue: HashMap::new(), } } } @@ -139,6 +150,27 @@ impl FileExplorer { filtered.get(idx).copied() } + /// Enqueue a file for transfer + pub fn enqueue(&mut self, src: &Path, dst: &Path) { + self.transfer_queue + .insert(PathBuf::from(src), PathBuf::from(dst)); + } + + /// Get enqueued files + pub fn enqueued(&self) -> &HashMap { + &self.transfer_queue + } + + /// Dequeue a file + pub fn dequeue(&mut self, src: &Path) { + self.transfer_queue.remove(src); + } + + /// Clear transfer queue + pub fn clear_queue(&mut self) { + self.transfer_queue.clear(); + } + // Formatting /// Format a file entry @@ -586,6 +618,26 @@ mod tests { assert_eq!(explorer.files.len(), 3); } + #[test] + fn test_should_enqueue_and_dequeue_files() { + let mut explorer: FileExplorer = FileExplorer::default(); + // Create files (files are then sorted by name) + explorer.set_files(vec![ + make_fs_entry("CONTRIBUTING.md", false), + make_fs_entry("docs", true), + make_fs_entry("src", true), + make_fs_entry("README.md", false), + ]); + // Enqueue + explorer.enqueue(Path::new("CONTRIBUTING.md"), Path::new("CONTRIBUTING.md")); + explorer.enqueue(Path::new("docs"), Path::new("docs")); + // Dequeue + explorer.dequeue(Path::new("CONTRIBUTING.md")); + assert_eq!(explorer.enqueued().len(), 1); + explorer.dequeue(Path::new("docs")); + assert_eq!(explorer.enqueued().len(), 0); + } + fn make_fs_entry(name: &str, is_dir: bool) -> File { let t: SystemTime = SystemTime::now(); let metadata = Metadata { diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index fc10cb4..6d2c577 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -21,6 +21,7 @@ pub(crate) mod edit; pub(crate) mod exec; pub(crate) mod filter; pub(crate) mod find; +pub(crate) mod mark; pub(crate) mod mkdir; pub(crate) mod newfile; pub(crate) mod open; @@ -64,7 +65,7 @@ impl SelectedFile { #[derive(Debug)] enum SelectedFileIndex { One(usize), - Many(Vec), + Many(Vec), // TODO: remove None, } diff --git a/src/ui/activities/filetransfer/components/transfer/file_list.rs b/src/ui/activities/filetransfer/components/transfer/file_list.rs index 87d922f..b5a65ce 100644 --- a/src/ui/activities/filetransfer/components/transfer/file_list.rs +++ b/src/ui/activities/filetransfer/components/transfer/file_list.rs @@ -17,20 +17,17 @@ const PROP_DOT_DOT: &str = "dot_dot"; /// OwnStates contains states for this component #[derive(Clone, Default)] struct OwnStates { - list_index: usize, // Index of selected element in list - selected: Vec, // Selected files + list_index: usize, // Index of selected element in list + list_len: usize, // Length of the list + dot_dot: bool, } impl OwnStates { /// Initialize list states pub fn init_list_states(&mut self, len: usize, has_dot_dot: bool) { - self.selected = Vec::with_capacity(len + if has_dot_dot { 1 } else { 0 }); + self.list_len = len + if has_dot_dot { 1 } else { 0 }; self.fix_list_index(); - } - - /// Return current value for list index - pub fn list_index(&self) -> usize { - self.list_index + self.dot_dot = has_dot_dot; } /// Incremenet list index. @@ -44,6 +41,14 @@ impl OwnStates { } } + pub fn real_index(&self) -> usize { + if self.dot_dot { + self.list_index.saturating_sub(1) + } else { + self.list_index + } + } + /// Decrement list index /// If `can_rewind` is `true` the index rewinds when boundary is reached pub fn decr_list_index(&mut self, can_rewind: bool) { @@ -68,22 +73,7 @@ impl OwnStates { /// Returns the length of the file list, which is actually the capacity of the selection vector pub fn list_len(&self) -> usize { - self.selected.capacity() - } - - /// Returns whether the file with index `entry` is selected - pub fn is_selected(&self, entry: usize) -> bool { - self.selected.contains(&entry) - } - - /// Returns whether the selection is currently empty - pub fn is_selection_empty(&self) -> bool { - self.selected.is_empty() - } - - /// Returns current file selection - pub fn get_selection(&self) -> Vec { - self.selected.clone() + self.list_len } /// Keep index if possible, otherwise set to lenght - 1 @@ -94,44 +84,6 @@ impl OwnStates { self.list_index = 0; } } - - // -- select manipulation - - /// Select or deselect file with provided entry index - pub fn toggle_file(&mut self, entry: usize) { - match self.is_selected(entry) { - true => self.deselect(entry), - false => self.select(entry), - } - // increment index - self.incr_list_index(false); - } - - /// Select all files - pub fn select_all(&mut self, has_dot_dot: bool) { - for i in 0..self.list_len() { - self.select(i + if has_dot_dot { 1 } else { 0 }); - } - } - - /// Select all files - pub fn deselect_all(&mut self) { - self.selected.clear(); - } - - /// Select provided index if not selected yet - fn select(&mut self, entry: usize) { - if !self.is_selected(entry) { - self.selected.push(entry); - } - } - - /// Remove element file with associated index - fn deselect(&mut self, entry: usize) { - if self.is_selected(entry) { - self.selected.retain(|&x| x != entry); - } - } } #[derive(Default)] @@ -222,27 +174,12 @@ impl MockComponent for FileList { Some(table) => init_table_iter .iter() .chain(table.iter()) - .enumerate() - .map(|(num, row)| { - let real_num = num; - let num = if self.has_dot_dot() { - num.checked_sub(1).unwrap_or_default() - } else { - num - }; - + .map(|row| { let columns: Vec = row .iter() .map(|col| { - let (fg, bg, mut modifiers) = + let (fg, bg, modifiers) = tui_realm_stdlib::utils::use_or_default_styles(&self.props, col); - if !(self.has_dot_dot() && real_num == 0) - && self.states.is_selected(num) - { - modifiers |= TextModifiers::REVERSED - | TextModifiers::UNDERLINED - | TextModifiers::ITALIC; - } Span::styled( col.content.clone(), @@ -302,20 +239,11 @@ impl MockComponent for FileList { return State::One(StateValue::String("..".to_string())); } - match self.states.is_selection_empty() { - true => State::One(StateValue::Usize(if self.has_dot_dot() { - self.states.list_index.checked_sub(1).unwrap_or_default() - } else { - self.states.list_index - })), - false => State::Vec( - self.states - .get_selection() - .into_iter() - .map(StateValue::Usize) - .collect(), - ), - } + State::One(StateValue::Usize(if self.has_dot_dot() { + self.states.list_index.checked_sub(1).unwrap_or_default() + } else { + self.states.list_index + })) } fn perform(&mut self, cmd: Cmd) -> CmdResult { @@ -374,25 +302,18 @@ impl MockComponent for FileList { CmdResult::None } } - Cmd::Custom(FILE_LIST_CMD_SELECT_ALL) => { - self.states.select_all(self.has_dot_dot()); - CmdResult::None - } - Cmd::Custom(FILE_LIST_CMD_DESELECT_ALL) => { - self.states.deselect_all(); - CmdResult::None - } Cmd::Toggle => { - if self.has_dot_dot() && self.states.list_index() == 0 { + if self.states.list_index == 0 && self.has_dot_dot() { return CmdResult::None; } - self.states.toggle_file(if self.has_dot_dot() { - self.states.list_index().checked_sub(1).unwrap_or_default() - } else { - self.states.list_index() - }); - CmdResult::None + let index = self.states.real_index(); + self.states.list_index = self + .states + .list_index + .saturating_add(1) + .min(self.states.list_len.saturating_sub(1)); + CmdResult::Changed(State::One(StateValue::Usize(index))) } _ => CmdResult::None, } diff --git a/src/ui/activities/filetransfer/components/transfer/mod.rs b/src/ui/activities/filetransfer/components/transfer/mod.rs index 07510bd..6428f23 100644 --- a/src/ui/activities/filetransfer/components/transfer/mod.rs +++ b/src/ui/activities/filetransfer/components/transfer/mod.rs @@ -132,21 +132,26 @@ impl ExplorerFuzzy { modifiers: KeyModifiers::CONTROL, }) => { let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL)); - Some(Msg::None) + Some(Msg::Ui(UiMsg::MarkAll)) } Event::Keyboard(KeyEvent { code: Key::Char('a'), modifiers: KeyModifiers::ALT, }) => { let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_DESELECT_ALL)); - Some(Msg::None) + Some(Msg::Ui(UiMsg::MarkClear)) } Event::Keyboard(KeyEvent { code: Key::Char('m'), modifiers: KeyModifiers::NONE, }) => { - let _ = self.perform(Cmd::Toggle); - Some(Msg::None) + let CmdResult::Changed(State::One(StateValue::Usize(index))) = + self.perform(Cmd::Toggle) + else { + return Some(Msg::None); + }; + + Some(Msg::Ui(UiMsg::MarkFile(index))) } Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { self.perform(Cmd::Change); @@ -277,21 +282,26 @@ impl Component for ExplorerFind { modifiers: KeyModifiers::CONTROL, }) => { let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL)); - Some(Msg::None) + Some(Msg::Ui(UiMsg::MarkAll)) } Event::Keyboard(KeyEvent { code: Key::Char('a'), modifiers: KeyModifiers::ALT, }) => { let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_DESELECT_ALL)); - Some(Msg::None) + Some(Msg::Ui(UiMsg::MarkClear)) } Event::Keyboard(KeyEvent { code: Key::Char('m'), modifiers: KeyModifiers::NONE, }) => { - let _ = self.perform(Cmd::Toggle); - Some(Msg::None) + let CmdResult::Changed(State::One(StateValue::Usize(index))) = + self.perform(Cmd::Toggle) + else { + return Some(Msg::None); + }; + + Some(Msg::Ui(UiMsg::MarkFile(index))) } // -- comp msg Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { @@ -410,21 +420,26 @@ impl Component for ExplorerLocal { modifiers: KeyModifiers::CONTROL, }) => { let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL)); - Some(Msg::None) + Some(Msg::Ui(UiMsg::MarkAll)) } Event::Keyboard(KeyEvent { code: Key::Char('a'), modifiers: KeyModifiers::ALT, }) => { let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_DESELECT_ALL)); - Some(Msg::None) + Some(Msg::Ui(UiMsg::MarkClear)) } Event::Keyboard(KeyEvent { code: Key::Char('m'), modifiers: KeyModifiers::NONE, }) => { - let _ = self.perform(Cmd::Toggle); - Some(Msg::None) + let CmdResult::Changed(State::One(StateValue::Usize(index))) = + self.perform(Cmd::Toggle) + else { + return Some(Msg::None); + }; + + Some(Msg::Ui(UiMsg::MarkFile(index))) } // -- comp msg Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { @@ -619,21 +634,26 @@ impl Component for ExplorerRemote { modifiers: KeyModifiers::CONTROL, }) => { let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL)); - Some(Msg::None) + Some(Msg::Ui(UiMsg::MarkAll)) } Event::Keyboard(KeyEvent { code: Key::Char('a'), modifiers: KeyModifiers::ALT, }) => { let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_DESELECT_ALL)); - Some(Msg::None) + Some(Msg::Ui(UiMsg::MarkClear)) } Event::Keyboard(KeyEvent { code: Key::Char('m'), modifiers: KeyModifiers::NONE, }) => { - let _ = self.perform(Cmd::Toggle); - Some(Msg::None) + let CmdResult::Changed(State::One(StateValue::Usize(index))) = + self.perform(Cmd::Toggle) + else { + return Some(Msg::None); + }; + + Some(Msg::Ui(UiMsg::MarkFile(index))) } // -- comp msg Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index 5564b02..ef1eabb 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -60,6 +60,35 @@ impl Browser { } } + pub fn other_explorer(&self) -> &FileExplorer { + let found_set = self.found.is_some(); + match (self.tab, found_set) { + (FileExplorerTab::HostBridge, false) => &self.remote, + (FileExplorerTab::Remote, false) => &self.host_bridge, + (FileExplorerTab::HostBridge, true) => &self.found.as_ref().unwrap().explorer, + (FileExplorerTab::Remote, true) => &self.found.as_ref().unwrap().explorer, + (FileExplorerTab::FindHostBridge, _) => &self.remote, + (FileExplorerTab::FindRemote, _) => &self.host_bridge, + } + } + + pub fn other_explorer_no_found(&self) -> &FileExplorer { + match self.tab { + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.remote, + FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.host_bridge, + } + } + + pub fn explorer_mut(&mut self) -> &mut FileExplorer { + match self.tab { + FileExplorerTab::HostBridge => &mut self.host_bridge, + FileExplorerTab::Remote => &mut self.remote, + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { + self.found.as_mut().map(|x| &mut x.explorer).unwrap() + } + } + } + pub fn host_bridge(&self) -> &FileExplorer { &self.host_bridge } diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index ec66fac..2ac2812 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -3,7 +3,8 @@ use std::path::{Path, PathBuf}; use bytesize::ByteSize; use tuirealm::props::{ - Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextSpan, + Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextModifiers, + TextSpan, }; use tuirealm::{PollStrategy, Update}; @@ -240,6 +241,11 @@ impl FileTransferActivity { /// 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() @@ -261,7 +267,15 @@ impl FileTransferActivity { let files: Vec> = self .host_bridge() .iter_files() - .map(|x| vec![TextSpan::from(self.host_bridge().fmt_file(x))]) + .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!( @@ -287,7 +301,10 @@ impl FileTransferActivity { /// Update remote file list pub(super) fn update_remote_filelist(&mut self) { self.reload_remote_dir(); + self.reload_remote_filelist(); + } + pub(super) fn reload_remote_filelist(&mut self) { let width = self .context_mut() .terminal() @@ -308,7 +325,15 @@ impl FileTransferActivity { let files: Vec> = self .remote() .iter_files() - .map(|x| vec![TextSpan::from(self.remote().fmt_file(x))]) + .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!( @@ -482,6 +507,15 @@ impl FileTransferActivity { } } + 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 => { diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 991f012..b8f58aa 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -153,6 +153,12 @@ enum UiMsg { FilterFiles(String), FuzzySearch(String), LogBackTabbed, + /// Mark file on the list; usize is the index of the file + MarkFile(usize), + /// Mark all file at tab + MarkAll, + /// Clear all marks + MarkClear, Quit, ReplacePopupTabbed, ShowChmodPopup, diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 9e5ce5e..4d4e8de 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -479,6 +479,15 @@ impl FileTransferActivity { UiMsg::LogBackTabbed => { assert!(self.app.active(&Id::ExplorerHostBridge).is_ok()); } + UiMsg::MarkFile(index) => { + self.action_mark_file(index); + } + UiMsg::MarkAll => { + self.action_mark_all(); + } + UiMsg::MarkClear => { + self.action_mark_clear(); + } UiMsg::Quit => { self.disconnect_and_quit(); self.umount_quit();