for Log {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
+ Event::Keyboard(KeyEvent {
+ code: Key::Right, ..
+ }) => Some(Msg::Ui(UiMsg::BottomPanelRight)),
+ Event::Keyboard(KeyEvent {
+ code: Key::Left, ..
+ }) => Some(Msg::Ui(UiMsg::BottomPanelLeft)),
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
diff --git a/src/ui/activities/filetransfer/components/mod.rs b/src/ui/activities/filetransfer/components/mod.rs
index 4076ab2..a74bb83 100644
--- a/src/ui/activities/filetransfer/components/mod.rs
+++ b/src/ui/activities/filetransfer/components/mod.rs
@@ -12,6 +12,7 @@ use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
mod log;
mod misc;
mod popups;
+mod selected_files;
mod transfer;
pub use misc::FooterBar;
@@ -26,6 +27,7 @@ pub use popups::{
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
pub use self::log::Log;
+pub use self::selected_files::SelectedFilesList;
#[derive(Default, MockComponent)]
pub struct GlobalListener {
diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs
index 32e5ee5..eb4f665 100644
--- a/src/ui/activities/filetransfer/components/popups.rs
+++ b/src/ui/activities/filetransfer/components/popups.rs
@@ -670,7 +670,7 @@ impl KeybindingsPopup {
))
.add_row()
.add_col(TextSpan::new("").bold().fg(key_color))
- .add_col(TextSpan::from(" Toggle log panel"))
+ .add_col(TextSpan::from(" Toggle bottom panel"))
.add_row()
.add_col(TextSpan::new("").bold().fg(key_color))
.add_col(TextSpan::from(" Quit termscp"))
diff --git a/src/ui/activities/filetransfer/components/selected_files.rs b/src/ui/activities/filetransfer/components/selected_files.rs
new file mode 100644
index 0000000..57f032d
--- /dev/null
+++ b/src/ui/activities/filetransfer/components/selected_files.rs
@@ -0,0 +1,126 @@
+use std::path::{Path, PathBuf};
+
+use tui_realm_stdlib::List;
+use tuirealm::command::{Cmd, Direction, Position};
+use tuirealm::event::{Key, KeyEvent};
+use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan};
+use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
+
+use crate::ui::activities::filetransfer::{MarkQueue, Msg, UiMsg};
+
+#[derive(MockComponent)]
+pub struct SelectedFilesList {
+ component: List,
+ paths: Vec,
+ queue: MarkQueue,
+}
+
+impl SelectedFilesList {
+ pub fn new(
+ paths: &[(PathBuf, PathBuf)],
+ queue: MarkQueue,
+ color: Color,
+ title: &'static str,
+ ) -> Self {
+ let enqueued_paths = paths
+ .iter()
+ .map(|(src, _)| src.clone())
+ .collect::>();
+
+ Self {
+ queue,
+ paths: enqueued_paths,
+ component: List::default()
+ .borders(Borders::default().color(color).modifiers(BorderType::Plain))
+ .rewind(true)
+ .scroll(true)
+ .step(4)
+ .highlighted_color(color)
+ .highlighted_str("➤ ")
+ .title(title, Alignment::Left)
+ .rows(
+ paths
+ .iter()
+ .map(|(src, dest)| {
+ vec![
+ TextSpan::from(Self::filename(src)),
+ TextSpan::from(" -> "),
+ TextSpan::from(Self::filename(dest)),
+ ]
+ })
+ .collect(),
+ ),
+ }
+ }
+
+ fn filename(p: &Path) -> String {
+ p.file_name()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .to_string()
+ }
+}
+
+impl Component for SelectedFilesList {
+ fn on(&mut self, ev: Event) -> Option {
+ match ev {
+ 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::Right, ..
+ }) => Some(Msg::Ui(UiMsg::BottomPanelRight)),
+ Event::Keyboard(KeyEvent {
+ code: Key::Left, ..
+ }) => Some(Msg::Ui(UiMsg::BottomPanelLeft)),
+ Event::Keyboard(KeyEvent {
+ code: Key::BackTab | Key::Tab | Key::Char('p'),
+ ..
+ }) => Some(Msg::Ui(UiMsg::LogBackTabbed)),
+ Event::Keyboard(KeyEvent {
+ code: Key::Enter | Key::Delete,
+ ..
+ }) => {
+ // unmark the selected file
+ let State::One(StateValue::Usize(idx)) = self.state() else {
+ return None;
+ };
+
+ let path = self.paths.get(idx)?;
+
+ Some(Msg::Ui(UiMsg::MarkRemove(self.queue, path.clone())))
+ }
+ _ => 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..6b62ad4 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, .. }) => {
@@ -508,7 +523,7 @@ impl Component for ExplorerLocal {
Event::Keyboard(KeyEvent {
code: Key::Char('p'),
modifiers: KeyModifiers::NONE,
- }) => Some(Msg::Ui(UiMsg::ShowLogPanel)),
+ }) => Some(Msg::Ui(UiMsg::GoToTransferQueue)),
Event::Keyboard(KeyEvent {
code: Key::Char('r') | Key::Function(6),
modifiers: KeyModifiers::NONE,
@@ -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, .. }) => {
@@ -717,7 +737,7 @@ impl Component for ExplorerRemote {
Event::Keyboard(KeyEvent {
code: Key::Char('p'),
modifiers: KeyModifiers::NONE,
- }) => Some(Msg::Ui(UiMsg::ShowLogPanel)),
+ }) => Some(Msg::Ui(UiMsg::GoToTransferQueue)),
Event::Keyboard(KeyEvent {
code: Key::Char('r') | Key::Function(6),
modifiers: KeyModifiers::NONE,
diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs
index 5564b02..cca97b6 100644
--- a/src/ui/activities/filetransfer/lib/browser.rs
+++ b/src/ui/activities/filetransfer/lib/browser.rs
@@ -60,6 +60,23 @@ 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,
+ }
+ }
+
+ 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..4b15908 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};
@@ -227,7 +228,7 @@ impl FileTransferActivity {
transfer_stats
)
}
- TransferPayload::Many(entries) => {
+ TransferPayload::TransferQueue(entries) => {
format!(
"{} files has been successfully transferred ({})",
entries.len(),
@@ -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!(
@@ -460,7 +485,14 @@ impl FileTransferActivity {
.found()
.unwrap()
.iter_files()
- .map(|x| vec![TextSpan::from(self.found().unwrap().fmt_file(x))])
+ .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
@@ -482,6 +514,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..d097287 100644
--- a/src/ui/activities/filetransfer/mod.rs
+++ b/src/ui/activities/filetransfer/mod.rs
@@ -40,6 +40,12 @@ 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,
@@ -74,6 +80,8 @@ enum Id {
StatusBarRemote,
SymlinkPopup,
SyncBrowsingMkdirPopup,
+ TransferQueueHostBridge,
+ TransferQueueRemote,
WaitPopup,
WatchedPathsList,
WatcherPopup,
@@ -125,6 +133,8 @@ enum TransferMsg {
#[derive(Debug, PartialEq)]
enum UiMsg {
+ BottomPanelLeft,
+ BottomPanelRight,
ChangeFileSorting(FileSorting),
ChangeTransferWindow,
CloseChmodPopup,
@@ -153,6 +163,13 @@ enum UiMsg {
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,
ReplacePopupTabbed,
ShowChmodPopup,
@@ -165,7 +182,7 @@ enum UiMsg {
ShowFilterPopup,
ShowGotoPopup,
ShowKeybindingsPopup,
- ShowLogPanel,
+ GoToTransferQueue,
ShowMkdirPopup,
ShowNewFilePopup,
ShowOpenWithPopup,
@@ -307,6 +324,45 @@ impl FileTransferActivity {
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());
+ let dest = self.browser.other_explorer_no_found().wrkdir.clone();
+ 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(|_| {
diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs
index 4de9020..f2e34e0 100644
--- a/src/ui/activities/filetransfer/session.rs
+++ b/src/ui/activities/filetransfer/session.rs
@@ -41,7 +41,8 @@ enum TransferErrorReason {
pub(super) enum TransferPayload {
File(File),
Any(File),
- Many(Vec),
+ /// List of file with their destination name
+ TransferQueue(Vec<(File, PathBuf)>),
}
impl FileTransferActivity {
@@ -264,8 +265,8 @@ impl FileTransferActivity {
TransferPayload::File(ref file) => {
self.filetransfer_send_file(file, curr_remote_path, dst_name)
}
- TransferPayload::Many(ref entries) => {
- self.filetransfer_send_many(entries, curr_remote_path)
+ TransferPayload::TransferQueue(ref entries) => {
+ self.filetransfer_send_transfer_queue(entries)
}
};
// Notify
@@ -331,18 +332,17 @@ impl FileTransferActivity {
result
}
- /// Send many entries to remote
- fn filetransfer_send_many(
+ /// Send transfer queue entries to remote
+ fn filetransfer_send_transfer_queue(
&mut self,
- entries: &[File],
- curr_remote_path: &Path,
+ 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_host(x))
+ .map(|(x, _)| self.get_total_transfer_size_host(x))
.sum();
self.transfer.full.init(total_transfer_size);
// Mount progress bar
@@ -350,7 +350,7 @@ impl FileTransferActivity {
// Send recurse
let result = entries
.iter()
- .map(|x| self.filetransfer_send_recurse(x, curr_remote_path, None))
+ .map(|(x, remote)| self.filetransfer_send_recurse(x, remote, None))
.find(|x| x.is_err())
.unwrap_or(Ok(()));
// Umount progress bar
@@ -704,8 +704,8 @@ impl FileTransferActivity {
self.filetransfer_recv_any(entry, host_bridge_path, dst_name)
}
TransferPayload::File(ref file) => self.filetransfer_recv_file(file, host_bridge_path),
- TransferPayload::Many(ref entries) => {
- self.filetransfer_recv_many(entries, host_bridge_path)
+ TransferPayload::TransferQueue(ref entries) => {
+ self.filetransfer_recv_transfer_queue(entries)
}
};
// Notify
@@ -764,18 +764,17 @@ impl FileTransferActivity {
result.map_err(|x| x.to_string())
}
- /// Send many entries to remote
- fn filetransfer_recv_many(
+ /// Receive transfer queue from remote
+ fn filetransfer_recv_transfer_queue(
&mut self,
- entries: &[File],
- curr_remote_path: &Path,
+ 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_remote(x))
+ .map(|(x, _)| self.get_total_transfer_size_remote(x))
.sum();
self.transfer.full.init(total_transfer_size);
// Mount progress bar
@@ -783,7 +782,7 @@ impl FileTransferActivity {
// Send recurse
let result = entries
.iter()
- .map(|x| self.filetransfer_recv_recurse(x, curr_remote_path, None))
+ .map(|(x, path)| self.filetransfer_recv_recurse(x, path, None))
.find(|x| x.is_err())
.unwrap_or(Ok(()));
// Umount progress bar
diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs
index 9e5ce5e..25d4db2 100644
--- a/src/ui/activities/filetransfer/update.rs
+++ b/src/ui/activities/filetransfer/update.rs
@@ -11,7 +11,9 @@ use tuirealm::{State, StateValue, Update};
use super::actions::SelectedFile;
use super::actions::walkdir::WalkdirError;
use super::browser::{FileExplorerTab, FoundExplorerTab};
-use super::{ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg};
+use super::{
+ ExitReason, FileTransferActivity, Id, MarkQueue, Msg, TransferMsg, TransferOpts, UiMsg,
+};
impl Update for FileTransferActivity {
fn update(&mut self, msg: Option) -> Option {
@@ -113,7 +115,7 @@ impl FileTransferActivity {
}
}
TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::HostBridge => {
- if let SelectedFile::One(entry) = self.get_local_selected_entries() {
+ if let Some(entry) = self.get_local_selected_file() {
self.action_submit_local(entry);
// Update file list if sync
if self.browser.sync_browsing && self.browser.found().is_none() {
@@ -123,7 +125,7 @@ impl FileTransferActivity {
}
}
TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => {
- if let SelectedFile::One(entry) = self.get_remote_selected_entries() {
+ 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() {
@@ -473,12 +475,33 @@ impl FileTransferActivity {
self.browser.fuzzy_search(&needle);
self.update_find_list();
}
- UiMsg::ShowLogPanel => {
- assert!(self.app.active(&Id::Log).is_ok());
+ UiMsg::GoToTransferQueue => {
+ assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok());
}
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::MarkRemove(tab, path) => match tab {
+ MarkQueue::Local => {
+ self.host_bridge_mut().dequeue(&path);
+ self.reload_host_bridge_filelist();
+ self.refresh_host_bridge_transfer_queue();
+ }
+ MarkQueue::Remote => {
+ self.remote_mut().dequeue(&path);
+ self.reload_remote_filelist();
+ self.refresh_remote_transfer_queue();
+ }
+ },
UiMsg::Quit => {
self.disconnect_and_quit();
self.umount_quit();
@@ -584,6 +607,31 @@ impl FileTransferActivity {
UiMsg::WindowResized => {
self.redraw = true;
}
+
+ UiMsg::BottomPanelLeft => match self.app.focus() {
+ Some(Id::TransferQueueHostBridge) => {
+ assert!(self.app.active(&Id::Log).is_ok())
+ }
+ Some(Id::TransferQueueRemote) => {
+ assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok())
+ }
+ Some(Id::Log) => {
+ assert!(self.app.active(&Id::TransferQueueRemote).is_ok())
+ }
+ _ => {}
+ },
+ UiMsg::BottomPanelRight => match self.app.focus() {
+ Some(Id::TransferQueueHostBridge) => {
+ assert!(self.app.active(&Id::TransferQueueRemote).is_ok())
+ }
+ Some(Id::TransferQueueRemote) => {
+ assert!(self.app.active(&Id::Log).is_ok())
+ }
+ Some(Id::Log) => {
+ assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok())
+ }
+ _ => {}
+ },
}
None
}
diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs
index ccbd286..71ad8e9 100644
--- a/src/ui/activities/filetransfer/view.rs
+++ b/src/ui/activities/filetransfer/view.rs
@@ -16,6 +16,7 @@ use super::browser::{FileExplorerTab, FoundExplorerTab};
use super::components::ATTR_FILES;
use super::{Context, FileTransferActivity, Id, components};
use crate::explorer::FileSorting;
+use crate::ui::activities::filetransfer::MarkQueue;
use crate::utils::ui::{Popup, Size};
impl FileTransferActivity {
@@ -81,6 +82,8 @@ impl FileTransferActivity {
)
.is_ok()
);
+ self.refresh_host_bridge_transfer_queue();
+ self.refresh_remote_transfer_queue();
// Load status bar
self.refresh_local_status_bar();
self.refresh_remote_status_bar();
@@ -138,6 +141,17 @@ impl FileTransferActivity {
.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
@@ -153,8 +167,13 @@ impl FileTransferActivity {
} 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_chunks[1]);
+ self.app.view(&Id::Log, f, bottom_components[2]);
// Draw status bar
self.app
.view(&Id::StatusBarHostBridge, f, status_bar_chunks[0]);
@@ -928,6 +947,56 @@ impl FileTransferActivity {
let _ = self.app.umount(&Id::FileInfoPopup);
}
+ pub(super) fn refresh_host_bridge_transfer_queue(&mut self) {
+ let enqueued = self
+ .host_bridge()
+ .enqueued()
+ .iter()
+ .map(|(src, dest)| (src.clone(), dest.clone()))
+ .collect::>();
+ let log_panel = self.theme().transfer_log_window;
+
+ assert!(
+ self.app
+ .remount(
+ Id::TransferQueueHostBridge,
+ Box::new(components::SelectedFilesList::new(
+ &enqueued,
+ MarkQueue::Local,
+ log_panel,
+ "Host Bridge selected files",
+ )),
+ vec![]
+ )
+ .is_ok()
+ );
+ }
+
+ pub(super) fn refresh_remote_transfer_queue(&mut self) {
+ let enqueued = self
+ .remote()
+ .enqueued()
+ .iter()
+ .map(|(src, dest)| (src.clone(), dest.clone()))
+ .collect::>();
+ let log_panel = self.theme().transfer_log_window;
+
+ assert!(
+ self.app
+ .remount(
+ Id::TransferQueueRemote,
+ Box::new(components::SelectedFilesList::new(
+ &enqueued,
+ MarkQueue::Remote,
+ log_panel,
+ "Remote transfer selected files",
+ )),
+ vec![]
+ )
+ .is_ok()
+ );
+ }
+
pub(super) fn refresh_local_status_bar(&mut self) {
let sorting_color = self.theme().transfer_status_sorting;
let hidden_color = self.theme().transfer_status_hidden;