From 99fd0b199d27a8f82716f1f6c718b46aab62831b Mon Sep 17 00:00:00 2001 From: ChristianVisintin Date: Sat, 26 Dec 2020 21:47:48 +0100 Subject: [PATCH] FileTransferActivity: sort files with --- CHANGELOG.md | 1 + README.md | 57 ++++---- src/fs/explorer/mod.rs | 65 +++++++++ .../activities/filetransfer_activity/input.rs | 137 +++++++++++++----- .../filetransfer_activity/layout.rs | 58 +++++++- .../activities/filetransfer_activity/mod.rs | 1 + 6 files changed, 256 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c54bbc0..baa080d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ FIXME: Released on - Hidden files are now not shown by default; use `A` to show hidden files. - Keybindings: - `A`: Toggle hidden files + - `B`: Sort files by (name, size, creation time, modify time) - `N`: New file - Dependencies: - added `bitflags 1.2.1` diff --git a/README.md b/README.md index a8fdf56..e2c8f9b 100644 --- a/README.md +++ b/README.md @@ -277,34 +277,35 @@ You can access the SSH key storage, from configuration moving to the `SSH Keys` ## Keybindings ⌨ -| Key | Command | Reminder | -|---------------|-------------------------------------------------------|-----------| -| `` | Disconnect from remote; return to authentication page | | -| `` | Switch between log tab and explorer | | -| `` | Go to previous directory in stack | | -| `` | Move to remote explorer tab | | -| `` | Move to local explorer tab | | -| `` | Move up in selected list | | -| `` | Move down in selected list | | -| `` | Move up in selected list by 8 rows | | -| `` | Move down in selected list by 8 rows | | -| `` | Enter directory | | -| `` | Upload / download selected file | | -| `` | Toggle hidden files | All | -| `` | Copy file/directory | Copy | -| `` | Make directory | Directory | -| `` | Delete file (Same as `DEL`) | Erase | -| `` | Go to supplied path | Go to | -| `` | Show help | Help | -| `` | Show info about selected file or directory | Info | -| `` | Reload current directory's content | List | -| `` | Create new file with provided name | New | -| `` | Edit file; see [Text editor](#text-editor-) | Open | -| `` | Quit TermSCP | Quit | -| `` | Rename file | Rename | -| `` | Go to parent directory | Upper | -| `` | Delete file | | -| `` | Abort file transfer process | | +| Key | Command | Reminder | +|---------------|-------------------------------------------------------|-------------| +| `` | Disconnect from remote; return to authentication page | | +| `` | Switch between log tab and explorer | | +| `` | Go to previous directory in stack | | +| `` | Move to remote explorer tab | | +| `` | Move to local explorer tab | | +| `` | Move up in selected list | | +| `` | Move down in selected list | | +| `` | Move up in selected list by 8 rows | | +| `` | Move down in selected list by 8 rows | | +| `` | Enter directory | | +| `` | Upload / download selected file | | +| `` | Toggle hidden files | All | +| `` | Sort files by | Bubblesort? | +| `` | Copy file/directory | Copy | +| `` | Make directory | Directory | +| `` | Delete file (Same as `DEL`) | Erase | +| `` | Go to supplied path | Go to | +| `` | Show help | Help | +| `` | Show info about selected file or directory | Info | +| `` | Reload current directory's content | List | +| `` | Create new file with provided name | New | +| `` | Edit file; see [Text editor](#text-editor-) | Open | +| `` | Quit TermSCP | Quit | +| `` | Rename file | Rename | +| `` | Go to parent directory | Upper | +| `` | Delete file | | +| `` | Abort file transfer process | | --- diff --git a/src/fs/explorer/mod.rs b/src/fs/explorer/mod.rs index 565fa35..3d4e735 100644 --- a/src/fs/explorer/mod.rs +++ b/src/fs/explorer/mod.rs @@ -52,6 +52,7 @@ pub enum FileSorting { ByName, ByModifyTime, ByCreationTime, + BySize, } /// ## GroupDirs @@ -204,6 +205,7 @@ impl FileExplorer { FileSorting::ByName => self.sort_files_by_name(), FileSorting::ByCreationTime => self.sort_files_by_creation_time(), FileSorting::ByModifyTime => self.sort_files_by_mtime(), + FileSorting::BySize => self.sort_files_by_size(), } // Directories first (NOTE: MUST COME AFTER OTHER SORTING) // Group directories if necessary @@ -240,6 +242,14 @@ impl FileExplorer { .sort_by(|a: &FsEntry, b: &FsEntry| b.get_creation_time().cmp(&a.get_creation_time())); } + /// ### sort_files_by_size + /// + /// Sort files by size + fn sort_files_by_size(&mut self) { + self.files + .sort_by(|a: &FsEntry, b: &FsEntry| b.get_size().cmp(&a.get_size())); + } + /// ### sort_files_directories_first /// /// Sort files; directories come first @@ -442,6 +452,7 @@ impl ToString for FileSorting { FileSorting::ByCreationTime => "by_creation_time", FileSorting::ByModifyTime => "by_mtime", FileSorting::ByName => "by_name", + FileSorting::BySize => "by_size", }) } } @@ -453,6 +464,7 @@ impl FromStr for FileSorting { "by_creation_time" => Ok(FileSorting::ByCreationTime), "by_mtime" => Ok(FileSorting::ByModifyTime), "by_name" => Ok(FileSorting::ByName), + "by_size" => Ok(FileSorting::BySize), _ => Err(()), } } @@ -733,6 +745,22 @@ mod tests { assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); } + #[test] + fn test_fs_explorer_sort_by_size() { + let mut explorer: FileExplorer = FileExplorer::default(); + // Create files (files are then sorted by name) + explorer.set_files(vec![ + make_fs_entry_with_size("README.md", false, 1024), + make_fs_entry("src/", true), + make_fs_entry_with_size("CONTRIBUTING.md", false, 256), + ]); + explorer.sort_by(FileSorting::BySize); + // Directory has size 4096 + assert_eq!(explorer.files.get(0).unwrap().get_name(), "src/"); + assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); + assert_eq!(explorer.files.get(2).unwrap().get_name(), "CONTRIBUTING.md"); + } + #[test] fn test_fs_explorer_sort_by_name_and_dirs_first() { let mut explorer: FileExplorer = FileExplorer::default(); @@ -793,6 +821,7 @@ mod tests { assert_eq!(FileSorting::ByCreationTime.to_string(), "by_creation_time"); assert_eq!(FileSorting::ByModifyTime.to_string(), "by_mtime"); assert_eq!(FileSorting::ByName.to_string(), "by_name"); + assert_eq!(FileSorting::BySize.to_string(), "by_size"); assert_eq!( FileSorting::from_str("by_creation_time").ok().unwrap(), FileSorting::ByCreationTime @@ -805,6 +834,10 @@ mod tests { FileSorting::from_str("by_name").ok().unwrap(), FileSorting::ByName ); + assert_eq!( + FileSorting::from_str("by_size").ok().unwrap(), + FileSorting::BySize + ); assert!(FileSorting::from_str("omar").is_err()); // Group dirs assert_eq!(GroupDirs::First.to_string(), "first"); @@ -845,4 +878,36 @@ mod tests { }), } } + + fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> FsEntry { + let t_now: SystemTime = SystemTime::now(); + match is_dir { + false => FsEntry::File(FsFile { + name: name.to_string(), + abs_path: PathBuf::from(name), + last_change_time: t_now, + last_access_time: t_now, + creation_time: t_now, + size: size, + ftype: None, // File type + readonly: false, + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((6, 4, 4)), // UNIX only + }), + true => FsEntry::Directory(FsDirectory { + name: name.to_string(), + abs_path: PathBuf::from(name), + last_change_time: t_now, + last_access_time: t_now, + creation_time: t_now, + readonly: false, + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((7, 5, 5)), // UNIX only + }), + } + } } diff --git a/src/ui/activities/filetransfer_activity/input.rs b/src/ui/activities/filetransfer_activity/input.rs index 3e886c3..27b9c09 100644 --- a/src/ui/activities/filetransfer_activity/input.rs +++ b/src/ui/activities/filetransfer_activity/input.rs @@ -23,13 +23,15 @@ * */ +// Deps extern crate tempfile; - +// Local use super::{ DialogCallback, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputEvent, InputField, InputMode, LogLevel, OnInputSubmitCallback, PopupType, }; - +use crate::fs::explorer::{FileExplorer, FileSorting}; +// Ext use crossterm::event::{KeyCode, KeyModifiers}; use std::path::PathBuf; @@ -59,7 +61,7 @@ impl FileTransferActivity { /// ### handle_input_event /// /// Handle input event based on current input mode - pub(super) fn handle_input_event(&mut self, ev: &InputEvent) { + fn handle_input_event(&mut self, ev: &InputEvent) { // NOTE: this is necessary due to this // NOTE: Do you want my opinion about that issue? It's a bs and doesn't make any sense. let popup: Option = match &self.input_mode { @@ -79,7 +81,7 @@ impl FileTransferActivity { /// ### handle_input_event_mode_explorer /// /// Input event handler for explorer mode - pub(super) fn handle_input_event_mode_explorer(&mut self, ev: &InputEvent) { + fn handle_input_event_mode_explorer(&mut self, ev: &InputEvent) { // Match input field match self.input_field { InputField::Explorer => match self.tab { @@ -94,7 +96,7 @@ impl FileTransferActivity { /// ### handle_input_event_mode_explorer_tab_local /// /// Input event handler for explorer mode when localhost tab is selected - pub(super) fn handle_input_event_mode_explorer_tab_local(&mut self, ev: &InputEvent) { + fn handle_input_event_mode_explorer_tab_local(&mut self, ev: &InputEvent) { // Match events if let InputEvent::Key(key) = ev { match key.code { @@ -172,6 +174,10 @@ impl FileTransferActivity { // Toggle hidden files self.local.toggle_hidden_files(); } + 'b' | 'B' => { + // Choose file sorting type + self.input_mode = InputMode::Popup(PopupType::FileSortingDialog); + } 'c' | 'C' => { // Copy self.input_mode = InputMode::Popup(PopupType::Input( @@ -309,7 +315,7 @@ impl FileTransferActivity { /// ### handle_input_event_mode_explorer_tab_local /// /// Input event handler for explorer mode when remote tab is selected - pub(super) fn handle_input_event_mode_explorer_tab_remote(&mut self, ev: &InputEvent) { + fn handle_input_event_mode_explorer_tab_remote(&mut self, ev: &InputEvent) { // Match events if let InputEvent::Key(key) = ev { match key.code { @@ -387,6 +393,10 @@ impl FileTransferActivity { // Toggle hidden files self.remote.toggle_hidden_files(); } + 'b' | 'B' => { + // Choose file sorting type + self.input_mode = InputMode::Popup(PopupType::FileSortingDialog); + } 'c' | 'C' => { // Copy self.input_mode = InputMode::Popup(PopupType::Input( @@ -521,7 +531,7 @@ impl FileTransferActivity { /// ### handle_input_event_mode_explorer_log /// /// Input even handler for explorer mode when log tab is selected - pub(super) fn handle_input_event_mode_explorer_log(&mut self, ev: &InputEvent) { + fn handle_input_event_mode_explorer_log(&mut self, ev: &InputEvent) { // Match event let records_block: usize = 16; if let InputEvent::Key(key) = ev { @@ -580,12 +590,13 @@ impl FileTransferActivity { /// ### handle_input_event_mode_explorer /// /// Input event handler for popup mode. Handler is then based on Popup type - pub(super) fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, popup: PopupType) { + fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, popup: PopupType) { match popup { PopupType::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev), PopupType::FileInfo => self.handle_input_event_mode_popup_fileinfo(ev), - PopupType::Help => self.handle_input_event_mode_popup_help(ev), PopupType::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev), + PopupType::FileSortingDialog => self.handle_input_event_mode_popup_file_sorting(ev), + PopupType::Help => self.handle_input_event_mode_popup_help(ev), PopupType::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb), PopupType::Progress(_) => self.handle_input_event_mode_popup_progress(ev), PopupType::Wait(_) => self.handle_input_event_mode_popup_wait(ev), @@ -598,7 +609,7 @@ impl FileTransferActivity { /// ### handle_input_event_mode_popup_alert /// /// Input event handler for popup alert - pub(super) fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) { + fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) { // If enter, close popup if let InputEvent::Key(key) = ev { if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { @@ -611,20 +622,7 @@ impl FileTransferActivity { /// ### handle_input_event_mode_popup_fileinfo /// /// Input event handler for popup fileinfo - pub(super) fn handle_input_event_mode_popup_fileinfo(&mut self, ev: &InputEvent) { - // If enter, close popup - if let InputEvent::Key(key) = ev { - if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { - // Set input mode back to explorer - self.input_mode = InputMode::Explorer; - } - } - } - - /// ### handle_input_event_mode_popup_help - /// - /// Input event handler for popup help - pub(super) fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) { + fn handle_input_event_mode_popup_fileinfo(&mut self, ev: &InputEvent) { // If enter, close popup if let InputEvent::Key(key) = ev { if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { @@ -637,7 +635,7 @@ impl FileTransferActivity { /// ### handle_input_event_mode_popup_fatal /// /// Input event handler for popup alert - pub(super) fn handle_input_event_mode_popup_fatal(&mut self, ev: &InputEvent) { + fn handle_input_event_mode_popup_fatal(&mut self, ev: &InputEvent) { // If enter, close popup if let InputEvent::Key(key) = ev { if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { @@ -647,14 +645,61 @@ impl FileTransferActivity { } } + /// ### handle_input_event_mode_popup_file_sorting + /// + /// Handle input event for file sorting dialog popup + fn handle_input_event_mode_popup_file_sorting(&mut self, ev: &InputEvent) { + // Match key code + if let InputEvent::Key(key) = ev { + match key.code { + KeyCode::Esc | KeyCode::Enter => { + // Exit + self.input_mode = InputMode::Explorer; + } + KeyCode::Right => { + // Update sorting mode + match self.tab { + FileExplorerTab::Local => { + Self::move_sorting_mode_opt_right(&mut self.local); + } + FileExplorerTab::Remote => { + Self::move_sorting_mode_opt_right(&mut self.remote); + } + } + } + KeyCode::Left => { + // Update sorting mode + match self.tab { + FileExplorerTab::Local => { + Self::move_sorting_mode_opt_left(&mut self.local); + } + FileExplorerTab::Remote => { + Self::move_sorting_mode_opt_left(&mut self.remote); + } + } + } + _ => { /* Nothing to do */ } + } + } + } + + /// ### handle_input_event_mode_popup_help + /// + /// Input event handler for popup help + fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) { + // If enter, close popup + if let InputEvent::Key(key) = ev { + if matches!(key.code, KeyCode::Esc | KeyCode::Enter) { + // Set input mode back to explorer + self.input_mode = InputMode::Explorer; + } + } + } + /// ### handle_input_event_mode_popup_input /// /// Input event handler for input popup - pub(super) fn handle_input_event_mode_popup_input( - &mut self, - ev: &InputEvent, - cb: OnInputSubmitCallback, - ) { + fn handle_input_event_mode_popup_input(&mut self, ev: &InputEvent, cb: OnInputSubmitCallback) { // If enter, close popup, otherwise push chars to input if let InputEvent::Key(key) = ev { match key.code { @@ -687,7 +732,7 @@ impl FileTransferActivity { /// ### handle_input_event_mode_popup_progress /// /// Input event handler for popup alert - pub(super) fn handle_input_event_mode_popup_progress(&mut self, ev: &InputEvent) { + fn handle_input_event_mode_popup_progress(&mut self, ev: &InputEvent) { if let InputEvent::Key(key) = ev { if let KeyCode::Char(ch) = key.code { // If is 'C' and CTRL @@ -702,14 +747,14 @@ impl FileTransferActivity { /// ### handle_input_event_mode_popup_wait /// /// Input event handler for popup alert - pub(super) fn handle_input_event_mode_popup_wait(&mut self, _ev: &InputEvent) { + fn handle_input_event_mode_popup_wait(&mut self, _ev: &InputEvent) { // There's nothing you can do here I guess... maybe ctrl+c in the future idk } /// ### handle_input_event_mode_popup_yesno /// /// Input event handler for popup alert - pub(super) fn handle_input_event_mode_popup_yesno( + fn handle_input_event_mode_popup_yesno( &mut self, ev: &InputEvent, yes_cb: DialogCallback, @@ -735,4 +780,30 @@ impl FileTransferActivity { } } } + + /// ### move_sorting_mode_opt_left + /// + /// Perform on file sorting dialog + fn move_sorting_mode_opt_left(explorer: &mut FileExplorer) { + let curr_sorting: FileSorting = explorer.get_file_sorting(); + explorer.sort_by(match curr_sorting { + FileSorting::BySize => FileSorting::ByCreationTime, + FileSorting::ByCreationTime => FileSorting::ByModifyTime, + FileSorting::ByModifyTime => FileSorting::ByName, + FileSorting::ByName => FileSorting::BySize, // Wrap + }); + } + + /// ### move_sorting_mode_opt_left + /// + /// Perform on file sorting dialog + fn move_sorting_mode_opt_right(explorer: &mut FileExplorer) { + let curr_sorting: FileSorting = explorer.get_file_sorting(); + explorer.sort_by(match curr_sorting { + FileSorting::ByName => FileSorting::ByModifyTime, + FileSorting::ByModifyTime => FileSorting::ByCreationTime, + FileSorting::ByCreationTime => FileSorting::BySize, + FileSorting::BySize => FileSorting::ByName, // Wrap + }); + } } diff --git a/src/ui/activities/filetransfer_activity/layout.rs b/src/ui/activities/filetransfer_activity/layout.rs index 95afa4d..fe6c33b 100644 --- a/src/ui/activities/filetransfer_activity/layout.rs +++ b/src/ui/activities/filetransfer_activity/layout.rs @@ -23,17 +23,19 @@ * */ +// Deps extern crate bytesize; extern crate hostname; #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] extern crate users; - +// Local use super::{ Context, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputField, InputMode, LogLevel, LogRecord, PopupType, }; +use crate::fs::explorer::{FileExplorer, FileSorting}; use crate::utils::fmt::{align_text_center, fmt_time}; - +// Ext use bytesize::ByteSize; use std::path::{Path, PathBuf}; use tui::{ @@ -105,6 +107,7 @@ impl FileTransferActivity { PopupType::Alert(_, _) => (50, 10), PopupType::Fatal(_) => (50, 10), PopupType::FileInfo => (50, 50), + PopupType::FileSortingDialog => (50, 10), PopupType::Help => (50, 80), PopupType::Input(_, _) => (40, 10), PopupType::Progress(_) => (40, 10), @@ -123,6 +126,9 @@ impl FileTransferActivity { popup_area, ), PopupType::FileInfo => f.render_widget(self.draw_popup_fileinfo(), popup_area), + PopupType::FileSortingDialog => { + f.render_widget(self.draw_popup_file_sorting_dialog(), popup_area) + } PopupType::Help => f.render_widget(self.draw_popup_help(), popup_area), PopupType::Input(txt, _) => { f.render_widget(self.draw_popup_input(txt.clone()), popup_area); @@ -367,6 +373,44 @@ impl FileTransferActivity { .start_corner(Corner::TopLeft) .style(Style::default().fg(Color::Red)) } + + /// ### draw_popup_file_sorting_dialog + /// + /// Draw FileSorting mode select popup + pub(super) fn draw_popup_file_sorting_dialog(&self) -> Tabs { + let choices: Vec = vec![ + Spans::from("Name"), + Spans::from("Modify time"), + Spans::from("Creation time"), + Spans::from("Size"), + ]; + let explorer: &FileExplorer = match self.tab { + FileExplorerTab::Local => &self.local, + FileExplorerTab::Remote => &self.remote, + }; + let index: usize = match explorer.get_file_sorting() { + FileSorting::ByCreationTime => 2, + FileSorting::ByModifyTime => 1, + FileSorting::ByName => 0, + FileSorting::BySize => 3, + }; + Tabs::new(choices) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title("Sort files by"), + ) + .select(index) + .style(Style::default()) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .bg(Color::LightMagenta) + .fg(Color::DarkGray), + ) + } + /// ### draw_popup_input /// /// Draw input popup @@ -727,6 +771,16 @@ impl FileTransferActivity { Span::raw(" "), Span::raw("Toggle hidden files"), ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Change file sorting mode"), + ])), ListItem::new(Spans::from(vec![ Span::styled( "", diff --git a/src/ui/activities/filetransfer_activity/mod.rs b/src/ui/activities/filetransfer_activity/mod.rs index e727008..c2029da 100644 --- a/src/ui/activities/filetransfer_activity/mod.rs +++ b/src/ui/activities/filetransfer_activity/mod.rs @@ -97,6 +97,7 @@ enum PopupType { Alert(Color, String), // Block color; Block text Fatal(String), // Must quit after being hidden FileInfo, // Show info about current file + FileSortingDialog, // Dialog for choosing file sorting type Help, // Show Help Input(String, OnInputSubmitCallback), // Input description; Callback for submit Progress(String), // Progress block text