feat: issue 256 - filter files (#266)

This commit is contained in:
Christian Visintin
2024-07-15 15:08:22 +02:00
committed by GitHub
parent 65aed76605
commit 631f09b9a8
19 changed files with 222 additions and 12 deletions

View File

@@ -43,6 +43,7 @@ Released on ??
- [Issue 226](https://github.com/veeso/termscp/issues/226): Use ssh-agent
- [Issue 241](https://github.com/veeso/termscp/issues/241): Jump to next entry after select
- [Issue 255](https://github.com/veeso/termscp/issues/255): New keybindings `Ctrl + Shift + A` to deselect all files
- [Issue 256](https://github.com/veeso/termscp/issues/256): Filter files in current folder. You can now filter files by pressing `/`. Both wildmatch and regex are accepted to filter files.
- [Issue 257](https://github.com/veeso/termscp/issues/257): CLI remote args cannot handle '@' in the username
## 0.13.0

1
Cargo.lock generated
View File

@@ -3553,6 +3553,7 @@ dependencies = [
"open",
"pretty_assertions",
"rand",
"regex",
"remotefs",
"remotefs-aws-s3",
"remotefs-ftp",

View File

@@ -55,6 +55,7 @@ notify = "=4.0.17"
notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
open = "^5.0"
rand = "^0.8.5"
regex = "^1"
remotefs = "^0.2.0"
remotefs-aws-s3 = { version = "^0.2.4", default-features = false, features = [
"find",

View File

@@ -244,6 +244,7 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
| `<X>` | Ejecutar un comando | eXecute |
| `<Y>` | Alternar navegación sincronizada | sYnc |
| `<Z>` | Cambiar ppermisos de archivo | |
| `</>` | Filtrar archivos (se admite tanto regex como coincidencias con comodines) | |
| `<CTRL+A>` | Seleccionar todos los archivos | |
| `<ALT+A>` | Deseleccionar todos los archivos | |
| `<CTRL+C>` | Abortar el proceso de transferencia de archivos | |

View File

@@ -243,6 +243,7 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
| `<X>` | Exécuter une commande | eXecute |
| `<Y>` | Basculer la navigation synchronisée | sYnc |
| `<Z>` | Changer permissions de fichier | |
| `</>` | Filtrer les fichiers (les expressions régulières et les correspondances génériques sont prises en charge) | |
| `<CTRL+A>` | Sélectionner tous les fichiers | |
| `<ALT+A>` | Desélectionner tous les fichiers | |
| `<CTRL+C>` | Abandonner le processus de transfert de fichiers | |

View File

@@ -239,6 +239,7 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
| `<X>` | Esegui comando shell | eXecute |
| `<Y>` | Abilita/disabilita Sync-Browsing | sYnc |
| `<Z>` | Modifica permessi file | |
| `</>` | Filtra i file (supporta sia regex che wildmatch ) | |
| `<CTRL+A>` | Seleziona tutti i file | |
| `<ALT+A>` | Deseleziona tutti i file | |
| `<CTRL+C>` | Annulla trasferimento file | |

View File

@@ -255,6 +255,7 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
| `<X>` | Execute a command | eXecute |
| `<Y>` | Toggle synchronized browsing | sYnc |
| `<Z>` | Change file mode | |
| `</>` | Filter files (both regex and wildmatch is supported) | |
| `<CTRL+A>` | Select all files | |
| `<ALT+A>` | Deselect all files | |
| `<CTRL+C>` | Abort file transfer process | |

View File

@@ -257,6 +257,7 @@ Para trocar de painel, você precisa pressionar `<LEFT>` para mover para o paine
| `<X>` | Executar um comando | Executar |
| `<Y>` | Alternar navegação sincronizada | Sincronizar |
| `<Z>` | Alterar modo de arquivo | |
| `</>` | Filtrar arquivos (suporte tanto para regex quanto para coringa) | |
| `<CTRL+A>` | Selecionar todos os arquivos | |
| `<ALT+A>` | Deselecionar todos os arquivos | |
| `<CTRL+C>` | Abortir processo de transferência de arquivo | |

View File

@@ -239,6 +239,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
| `<X>` | 运行命令 | eXecute |
| `<Y>` | 是否开启同步浏览 | sYnc |
| `<Z>` | 更改文件权限 | |
| `</>` | 过滤文件(支持正则表达式和通配符匹配) | |
| `<CTRL+A>` | 选中所有文件 | |
| `<ALT+A>` | 取消选择所有文件 | |
| `<CTRL+C>` | 终止文件传输 | |

View File

@@ -98,13 +98,6 @@ impl FileExplorer {
}
}
/*
/// Return amount of files
pub fn count(&self) -> usize {
self.files.len()
}
*/
/// Iterate over files
/// Filters are applied based on current options (e.g. hidden files not returned)
pub fn iter_files(&self) -> impl Iterator<Item = &File> + '_ {

View File

@@ -0,0 +1,51 @@
use std::str::FromStr;
use regex::Regex;
use remotefs::File;
use wildmatch::WildMatch;
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
use crate::ui::activities::filetransfer::FileTransferActivity;
#[derive(Clone, Debug)]
pub enum Filter {
Regex(Regex),
Wildcard(WildMatch),
}
impl FromStr for Filter {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
// try as regex
if let Ok(regex) = Regex::new(s) {
Ok(Self::Regex(regex))
} else {
Ok(Self::Wildcard(WildMatch::new(s)))
}
}
}
impl Filter {
fn matches(&self, s: &str) -> bool {
debug!("matching '{s}' with {:?}", self);
match self {
Self::Regex(re) => re.is_match(s),
Self::Wildcard(wm) => wm.matches(s),
}
}
}
impl FileTransferActivity {
pub fn filter(&self, filter: &str) -> Vec<File> {
let filter = Filter::from_str(filter).unwrap();
match self.browser.tab() {
FileExplorerTab::Local => self.browser.local().iter_files(),
FileExplorerTab::Remote => self.browser.remote().iter_files(),
_ => return vec![],
}
.filter(|f| filter.matches(&f.name()))
.cloned()
.collect()
}
}

View File

@@ -19,6 +19,7 @@ pub(crate) mod copy;
pub(crate) mod delete;
pub(crate) mod edit;
pub(crate) mod exec;
pub(crate) mod filter;
pub(crate) mod find;
pub(crate) mod mkdir;
pub(crate) mod newfile;

View File

@@ -17,8 +17,8 @@ mod transfer;
pub use misc::FooterBar;
pub use popups::{
ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup,
FileInfoPopup, FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup,
ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
FileInfoPopup, FilterPopup, FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WatchedPathsList, WatcherPopup,
};

View File

@@ -111,6 +111,90 @@ impl Component<Msg, NoUserEvent> for CopyPopup {
}
}
#[derive(MockComponent)]
pub struct FilterPopup {
component: Input,
}
impl FilterPopup {
pub fn new(color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.input_type(InputType::Text)
.placeholder(
"regex or wildmatch",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("Filter files by regex or wildmatch", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for FilterPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
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::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
..
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.state() {
State::One(StateValue::String(filter)) => Some(Msg::Ui(UiMsg::FilterFiles(filter))),
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseFilterPopup))
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct DeletePopup {
component: Radio,
@@ -790,6 +874,9 @@ impl KeybindingsPopup {
.add_col(TextSpan::new("<Z>").bold().fg(key_color))
.add_col(TextSpan::from(" Change file permissions"))
.add_row()
.add_col(TextSpan::new("</>").bold().fg(key_color))
.add_col(TextSpan::from(" Filter files"))
.add_row()
.add_col(TextSpan::new("<DEL|F8|E>").bold().fg(key_color))
.add_col(TextSpan::from(" Delete selected file"))
.add_row()

View File

@@ -330,6 +330,10 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
code: Key::Char('z'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowChmodPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('/'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFilterPopup)),
_ => None,
}
}
@@ -522,6 +526,10 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
code: Key::Char('z'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowChmodPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('/'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFilterPopup)),
_ => None,
}
}

View File

@@ -260,6 +260,7 @@ impl FileTransferActivity {
/// Update remote file list
pub(super) fn update_remote_filelist(&mut self) {
self.reload_remote_dir();
let width = self
.context_mut()
.terminal()

View File

@@ -49,6 +49,7 @@ enum Id {
ExplorerRemote,
FatalPopup,
FileInfoPopup,
FilterPopup,
FindPopup,
FooterBar,
GlobalListener,
@@ -130,6 +131,7 @@ enum UiMsg {
CloseFatalPopup,
CloseFileInfoPopup,
CloseFileSortingPopup,
CloseFilterPopup,
CloseFindExplorer,
CloseFindPopup,
CloseGotoPopup,
@@ -144,6 +146,7 @@ enum UiMsg {
CloseWatchedPathsList,
CloseWatcherPopup,
Disconnect,
FilterFiles(String),
LogBackTabbed,
Quit,
ReplacePopupTabbed,
@@ -154,6 +157,7 @@ enum UiMsg {
ShowExecPopup,
ShowFileInfoPopup,
ShowFileSortingPopup,
ShowFilterPopup,
ShowFindPopup,
ShowGotoPopup,
ShowKeybindingsPopup,

View File

@@ -400,6 +400,7 @@ impl FileTransferActivity {
}
UiMsg::CloseFileInfoPopup => self.umount_file_info(),
UiMsg::CloseFileSortingPopup => self.umount_file_sorting(),
UiMsg::CloseFilterPopup => self.umount_filter(),
UiMsg::CloseFindExplorer => {
self.finalize_find();
self.umount_find();
@@ -420,6 +421,33 @@ impl FileTransferActivity {
self.disconnect();
self.umount_disconnect();
}
UiMsg::FilterFiles(filter) => {
self.umount_filter();
let files = self.filter(&filter);
// Get wrkdir
let wrkdir = match self.browser.tab() {
FileExplorerTab::Local => self.local().wrkdir.clone(),
_ => self.remote().wrkdir.clone(),
};
// Create explorer and load files
self.browser.set_found(
match self.browser.tab() {
FileExplorerTab::Local => FoundExplorerTab::Local,
_ => FoundExplorerTab::Remote,
},
files,
wrkdir.as_path(),
);
// Mount result widget
self.mount_find(&filter);
self.update_find_list();
// Initialize tab
self.browser.change_tab(match self.browser.tab() {
FileExplorerTab::Local => FileExplorerTab::FindLocal,
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
_ => FileExplorerTab::FindLocal,
});
}
UiMsg::ShowLogPanel => {
assert!(self.app.active(&Id::Log).is_ok());
}
@@ -485,6 +513,7 @@ impl FileTransferActivity {
}
}
UiMsg::ShowFileSortingPopup => self.mount_file_sorting(),
UiMsg::ShowFilterPopup => self.mount_filter(),
UiMsg::ShowFindPopup => self.mount_find_input(),
UiMsg::ShowGotoPopup => self.mount_goto(),
UiMsg::ShowKeybindingsPopup => self.mount_help(),

View File

@@ -172,6 +172,11 @@ impl FileTransferActivity {
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::ChmodPopup, f, popup);
} else if self.app.mounted(&Id::FilterPopup) {
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.size());
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::FilterPopup, f, popup);
} else if self.app.mounted(&Id::FindPopup) {
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size());
f.render_widget(Clear, popup);
@@ -459,6 +464,23 @@ impl FileTransferActivity {
let _ = self.app.umount(&Id::ChmodPopup);
}
pub(super) fn umount_filter(&mut self) {
let _ = self.app.umount(&Id::FilterPopup);
}
pub(super) fn mount_filter(&mut self) {
let input_color = self.theme().misc_input_dialog;
assert!(self
.app
.remount(
Id::FilterPopup,
Box::new(components::FilterPopup::new(input_color)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::FilterPopup).is_ok());
}
pub(super) fn mount_copy(&mut self) {
let input_color = self.theme().misc_input_dialog;
assert!(self
@@ -1096,9 +1118,14 @@ impl FileTransferActivity {
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::ChmodPopup,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WaitPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WaitPopup,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::FilterPopup,
)))),
)),
)),
)),
)),