mirror of
https://github.com/veeso/termscp.git
synced 2025-12-06 17:15:35 -08:00
feat: issue 256 - filter files (#266)
This commit is contained in:
committed by
GitHub
parent
65aed76605
commit
631f09b9a8
@@ -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
1
Cargo.lock
generated
@@ -3553,6 +3553,7 @@ dependencies = [
|
||||
"open",
|
||||
"pretty_assertions",
|
||||
"rand",
|
||||
"regex",
|
||||
"remotefs",
|
||||
"remotefs-aws-s3",
|
||||
"remotefs-ftp",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 | |
|
||||
|
||||
@@ -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 | |
|
||||
|
||||
@@ -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 | |
|
||||
|
||||
@@ -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 | |
|
||||
|
||||
@@ -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 | |
|
||||
|
||||
@@ -239,6 +239,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
|
||||
| `<X>` | 运行命令 | eXecute |
|
||||
| `<Y>` | 是否开启同步浏览 | sYnc |
|
||||
| `<Z>` | 更改文件权限 | |
|
||||
| `</>` | 过滤文件(支持正则表达式和通配符匹配) | |
|
||||
| `<CTRL+A>` | 选中所有文件 | |
|
||||
| `<ALT+A>` | 取消选择所有文件 | |
|
||||
| `<CTRL+C>` | 终止文件传输 | |
|
||||
|
||||
@@ -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> + '_ {
|
||||
|
||||
51
src/ui/activities/filetransfer/actions/filter.rs
Normal file
51
src/ui/activities/filetransfer/actions/filter.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
)))),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
|
||||
Reference in New Issue
Block a user