mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
249 feature request better search results (#282)
* feat: issue 249 - fuzzy search replaced the old find explorer * fix: forgot to upload file * fix: removed debug
This commit is contained in:
committed by
GitHub
parent
c507d54700
commit
b2a8a3041c
@@ -41,8 +41,9 @@
|
|||||||
|
|
||||||
Released on
|
Released on
|
||||||
|
|
||||||
- [Issue 268](https://github.com/veeso/termscp/issues/268): Pods and container explorer for Kube protocol.
|
- [Issue 249](https://github.com/veeso/termscp/issues/249): The old *find* command has been replaced with a brand new explorer with support to 🪄 **Fuzzy search** 🪄. The command is still `<F>`.
|
||||||
- BREAKING ‼️ Kube address argument has changed; see manual!
|
- [Issue 268](https://github.com/veeso/termscp/issues/268): 📦 **Pods and container explorer** 🐳 for Kube protocol.
|
||||||
|
- BREAKING ‼️ Kube address argument has changed to `namespace[@<cluster_url>][$<path>]`
|
||||||
- Pod and container argumets have been removed; from now on you will connect with the following syntax to the provided namespace: `/pod-name/container-name/path/to/file`
|
- Pod and container argumets have been removed; from now on you will connect with the following syntax to the provided namespace: `/pod-name/container-name/path/to/file`
|
||||||
- [Issue 279](https://github.com/veeso/termscp/issues/279): do not clear screen
|
- [Issue 279](https://github.com/veeso/termscp/issues/279): do not clear screen
|
||||||
- [Issue 277](https://github.com/veeso/termscp/issues/277): Fix a bug in the configuration page, which caused being stuck if the added SSH key was empty
|
- [Issue 277](https://github.com/veeso/termscp/issues/277): Fix a bug in the configuration page, which caused being stuck if the added SSH key was empty
|
||||||
|
|||||||
65
Cargo.lock
generated
65
Cargo.lock
generated
@@ -461,6 +461,25 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.20"
|
version = "0.8.20"
|
||||||
@@ -1828,6 +1847,27 @@ dependencies = [
|
|||||||
"tauri-winrt-notification",
|
"tauri-winrt-notification",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nucleo"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4"
|
||||||
|
dependencies = [
|
||||||
|
"nucleo-matcher",
|
||||||
|
"parking_lot 0.12.3",
|
||||||
|
"rayon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nucleo-matcher"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num"
|
name = "num"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -2441,6 +2481,26 @@ dependencies = [
|
|||||||
"unicode-width 0.1.14",
|
"unicode-width 0.1.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -3497,6 +3557,7 @@ dependencies = [
|
|||||||
"magic-crypt",
|
"magic-crypt",
|
||||||
"notify",
|
"notify",
|
||||||
"notify-rust",
|
"notify-rust",
|
||||||
|
"nucleo",
|
||||||
"open",
|
"open",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -4167,9 +4228,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wildmatch"
|
name = "wildmatch"
|
||||||
version = "2.3.4"
|
version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3928939971918220fed093266b809d1ee4ec6c1a2d72692ff6876898f3b16c19"
|
checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
|
|||||||
17
Cargo.toml
17
Cargo.toml
@@ -5,13 +5,7 @@ description = "termscp is a feature rich terminal file transfer and explorer wit
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://termscp.veeso.dev"
|
homepage = "https://termscp.veeso.dev"
|
||||||
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||||
keywords = [
|
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
|
||||||
"scp-client",
|
|
||||||
"sftp-client",
|
|
||||||
"ftp-client",
|
|
||||||
"winscp",
|
|
||||||
"command-line-utility",
|
|
||||||
]
|
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "termscp"
|
name = "termscp"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -46,13 +40,18 @@ dirs = "^5.0"
|
|||||||
edit = "^0.1"
|
edit = "^0.1"
|
||||||
filetime = "^0.2"
|
filetime = "^0.2"
|
||||||
hostname = "^0.4"
|
hostname = "^0.4"
|
||||||
keyring = { version = "^3", optional = true, features = ["apple-native", "windows-native", "sync-secret-service"] }
|
keyring = { version = "^3", optional = true, features = [
|
||||||
|
"apple-native",
|
||||||
|
"windows-native",
|
||||||
|
"sync-secret-service",
|
||||||
|
] }
|
||||||
lazy-regex = "^3"
|
lazy-regex = "^3"
|
||||||
lazy_static = "^1"
|
lazy_static = "^1"
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
magic-crypt = "^3"
|
magic-crypt = "^3"
|
||||||
notify = "6"
|
notify = "6"
|
||||||
notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
|
notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
|
||||||
|
nucleo = "0.5"
|
||||||
open = "^5.0"
|
open = "^5.0"
|
||||||
rand = "^0.8.5"
|
rand = "^0.8.5"
|
||||||
regex = "^1"
|
regex = "^1"
|
||||||
@@ -83,7 +82,7 @@ tuirealm = "^1.9"
|
|||||||
unicode-width = "^0.2"
|
unicode-width = "^0.2"
|
||||||
version-compare = "^0.2"
|
version-compare = "^0.2"
|
||||||
whoami = "^1.5"
|
whoami = "^1.5"
|
||||||
wildmatch = "^2.3"
|
wildmatch = "^2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "^1"
|
pretty_assertions = "^1"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub enum FileSorting {
|
|||||||
ModifyTime,
|
ModifyTime,
|
||||||
CreationTime,
|
CreationTime,
|
||||||
Size,
|
Size,
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GroupDirs defines how directories should be grouped in sorting files
|
/// GroupDirs defines how directories should be grouped in sorting files
|
||||||
@@ -178,6 +179,7 @@ impl FileExplorer {
|
|||||||
FileSorting::CreationTime => self.sort_files_by_creation_time(),
|
FileSorting::CreationTime => self.sort_files_by_creation_time(),
|
||||||
FileSorting::ModifyTime => self.sort_files_by_mtime(),
|
FileSorting::ModifyTime => self.sort_files_by_mtime(),
|
||||||
FileSorting::Size => self.sort_files_by_size(),
|
FileSorting::Size => self.sort_files_by_size(),
|
||||||
|
FileSorting::None => {}
|
||||||
}
|
}
|
||||||
// Directories first (NOTE: MUST COME AFTER OTHER SORTING)
|
// Directories first (NOTE: MUST COME AFTER OTHER SORTING)
|
||||||
// Group directories if necessary
|
// Group directories if necessary
|
||||||
@@ -245,6 +247,7 @@ impl std::fmt::Display for FileSorting {
|
|||||||
FileSorting::ModifyTime => "by_mtime",
|
FileSorting::ModifyTime => "by_mtime",
|
||||||
FileSorting::Name => "by_name",
|
FileSorting::Name => "by_name",
|
||||||
FileSorting::Size => "by_size",
|
FileSorting::Size => "by_size",
|
||||||
|
FileSorting::None => "none",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ use super::super::browser::FileExplorerTab;
|
|||||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<File>, String> {
|
pub(crate) fn action_walkdir_local(&mut self) -> Result<Vec<File>, String> {
|
||||||
match self.host.find(input.as_str()) {
|
match self.host.find("*") {
|
||||||
Ok(entries) => Ok(entries),
|
Ok(entries) => Ok(entries),
|
||||||
Err(err) => Err(format!("Could not search for files: {err}")),
|
Err(err) => Err(format!("Could not search for files: {err}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<File>, String> {
|
pub(crate) fn action_walkdir_remote(&mut self) -> Result<Vec<File>, String> {
|
||||||
match self.client.as_mut().find(input.as_str()) {
|
match self.client.as_mut().find("*") {
|
||||||
Ok(entries) => Ok(entries),
|
Ok(entries) => Ok(entries),
|
||||||
Err(err) => Err(format!("Could not search for files: {err}")),
|
Err(err) => Err(format!("Could not search for files: {err}")),
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ impl FileTransferActivity {
|
|||||||
pub(crate) fn action_find_changedir(&mut self) {
|
pub(crate) fn action_find_changedir(&mut self) {
|
||||||
// Match entry
|
// Match entry
|
||||||
if let SelectedFile::One(entry) = self.get_found_selected_entries() {
|
if let SelectedFile::One(entry) = self.get_found_selected_entries() {
|
||||||
|
debug!("Changedir to: {}", entry.name());
|
||||||
// Get path: if a directory, use directory path; if it is a File, get parent path
|
// Get path: if a directory, use directory path; if it is a File, get parent path
|
||||||
let path = if entry.is_dir() {
|
let path = if entry.is_dir() {
|
||||||
entry.path().to_path_buf()
|
entry.path().to_path_buf()
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ mod transfer;
|
|||||||
pub use misc::FooterBar;
|
pub use misc::FooterBar;
|
||||||
pub use popups::{
|
pub use popups::{
|
||||||
ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup,
|
ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup,
|
||||||
FileInfoPopup, FilterPopup, FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
FileInfoPopup, FilterPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
||||||
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
||||||
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
|
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
|
||||||
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WatchedPathsList, WatcherPopup,
|
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WatchedPathsList, WatcherPopup,
|
||||||
};
|
};
|
||||||
pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote};
|
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
|
||||||
|
|
||||||
pub use self::log::Log;
|
pub use self::log::Log;
|
||||||
|
|
||||||
|
|||||||
@@ -583,89 +583,6 @@ impl Component<Msg, NoUserEvent> for FileInfoPopup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
|
||||||
pub struct FindPopup {
|
|
||||||
component: Input,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FindPopup {
|
|
||||||
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("*.txt", Style::default().fg(Color::Rgb(128, 128, 128)))
|
|
||||||
.title("Search files by name or wildmatch", Alignment::Center),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for FindPopup {
|
|
||||||
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(i)) => {
|
|
||||||
Some(Msg::Transfer(TransferMsg::SearchFile(i)))
|
|
||||||
}
|
|
||||||
_ => Some(Msg::None),
|
|
||||||
},
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
|
||||||
Some(Msg::Ui(UiMsg::CloseFindPopup))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(MockComponent)]
|
||||||
pub struct GoToPopup {
|
pub struct GoToPopup {
|
||||||
component: Input,
|
component: Input,
|
||||||
@@ -1675,6 +1592,7 @@ impl SortingPopup {
|
|||||||
FileSorting::ModifyTime => 1,
|
FileSorting::ModifyTime => 1,
|
||||||
FileSorting::Name => 0,
|
FileSorting::Name => 0,
|
||||||
FileSorting::Size => 3,
|
FileSorting::Size => 3,
|
||||||
|
FileSorting::None => 0,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1778,6 +1696,7 @@ fn file_sorting_label(sorting: FileSorting) -> &'static str {
|
|||||||
FileSorting::ModifyTime => "By modify time",
|
FileSorting::ModifyTime => "By modify time",
|
||||||
FileSorting::Name => "By name",
|
FileSorting::Name => "By name",
|
||||||
FileSorting::Size => "By size",
|
FileSorting::Size => "By size",
|
||||||
|
FileSorting::None => "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
use tui_realm_stdlib::Input;
|
||||||
|
use tuirealm::command::{Cmd, CmdResult};
|
||||||
|
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Table};
|
||||||
|
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||||
|
use tuirealm::{MockComponent, State};
|
||||||
|
|
||||||
|
use super::file_list::FileList;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Focus {
|
||||||
|
List,
|
||||||
|
#[default]
|
||||||
|
Search,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct OwnStates {
|
||||||
|
focus: Focus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwnStates {
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
self.focus = match self.focus {
|
||||||
|
Focus::List => Focus::Search,
|
||||||
|
Focus::Search => Focus::List,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FileListWithSearch {
|
||||||
|
file_list: FileList,
|
||||||
|
search: Input,
|
||||||
|
states: OwnStates,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileListWithSearch {
|
||||||
|
pub fn focus(&self) -> Focus {
|
||||||
|
self.states.focus
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn foreground(mut self, fg: Color) -> Self {
|
||||||
|
self.file_list
|
||||||
|
.attr(Attribute::Foreground, AttrValue::Color(fg));
|
||||||
|
self.search
|
||||||
|
.attr(Attribute::Foreground, AttrValue::Color(fg));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn background(mut self, bg: Color) -> Self {
|
||||||
|
self.file_list
|
||||||
|
.attr(Attribute::Background, AttrValue::Color(bg));
|
||||||
|
self.search
|
||||||
|
.attr(Attribute::Background, AttrValue::Color(bg));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn borders(mut self, b: Borders) -> Self {
|
||||||
|
self.file_list
|
||||||
|
.attr(Attribute::Borders, AttrValue::Borders(b.clone()));
|
||||||
|
self.search.attr(Attribute::Borders, AttrValue::Borders(b));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
|
||||||
|
self.file_list.attr(
|
||||||
|
Attribute::Title,
|
||||||
|
AttrValue::Title((t.as_ref().to_string(), a)),
|
||||||
|
);
|
||||||
|
self.search.attr(
|
||||||
|
Attribute::Title,
|
||||||
|
AttrValue::Title(("Fuzzy search".to_string(), a)),
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlighted_color(mut self, c: Color) -> Self {
|
||||||
|
self.file_list
|
||||||
|
.attr(Attribute::HighlightedColor, AttrValue::Color(c));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rows(mut self, rows: Table) -> Self {
|
||||||
|
self.file_list
|
||||||
|
.attr(Attribute::Content, AttrValue::Table(rows));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockComponent for FileListWithSearch {
|
||||||
|
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::tui::layout::Rect) {
|
||||||
|
// split the area in two
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(3), // Search
|
||||||
|
Constraint::Fill(1), // File list
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
// render the search input
|
||||||
|
self.search.view(frame, chunks[0]);
|
||||||
|
// render the file list
|
||||||
|
self.file_list.view(frame, chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||||
|
self.file_list.query(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||||
|
if attr == Attribute::Focus {
|
||||||
|
let value = value.unwrap_flag();
|
||||||
|
match value {
|
||||||
|
true => self.states.focus = Focus::Search,
|
||||||
|
false => self.states.focus = Focus::List,
|
||||||
|
}
|
||||||
|
self.search.attr(
|
||||||
|
Attribute::Focus,
|
||||||
|
AttrValue::Flag(self.states.focus == Focus::Search),
|
||||||
|
);
|
||||||
|
self.file_list.attr(
|
||||||
|
Attribute::Focus,
|
||||||
|
AttrValue::Flag(self.states.focus == Focus::List),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.file_list.attr(attr, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
match self.states.focus {
|
||||||
|
Focus::List => self.file_list.state(),
|
||||||
|
Focus::Search => self.search.state(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||||
|
match cmd {
|
||||||
|
Cmd::Change => {
|
||||||
|
self.states.next();
|
||||||
|
self.search.attr(
|
||||||
|
Attribute::Focus,
|
||||||
|
AttrValue::Flag(self.states.focus == Focus::Search),
|
||||||
|
);
|
||||||
|
self.file_list.attr(
|
||||||
|
Attribute::Focus,
|
||||||
|
AttrValue::Flag(self.states.focus == Focus::List),
|
||||||
|
);
|
||||||
|
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
cmd if self.states.focus == Focus::Search => self.search.perform(cmd),
|
||||||
|
cmd => self.file_list.perform(cmd),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,220 @@
|
|||||||
//!
|
//!
|
||||||
//! file transfer components
|
//! file transfer components
|
||||||
|
|
||||||
use super::{Msg, TransferMsg, UiMsg};
|
|
||||||
|
|
||||||
mod file_list;
|
mod file_list;
|
||||||
use file_list::FileList;
|
mod file_list_with_search;
|
||||||
use tuirealm::command::{Cmd, Direction, Position};
|
|
||||||
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
use tuirealm::props::{Alignment, Borders, Color, TextSpan};
|
use tuirealm::props::{Alignment, Borders, Color, TextSpan};
|
||||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent};
|
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
|
use self::file_list::FileList;
|
||||||
|
use self::file_list_with_search::FileListWithSearch;
|
||||||
|
use super::{Msg, TransferMsg, UiMsg};
|
||||||
|
|
||||||
|
#[derive(MockComponent)]
|
||||||
|
pub struct ExplorerFuzzy {
|
||||||
|
component: FileListWithSearch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExplorerFuzzy {
|
||||||
|
pub fn new<S: AsRef<str>>(title: S, files: &[&str], bg: Color, fg: Color, hg: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
component: FileListWithSearch::default()
|
||||||
|
.background(bg)
|
||||||
|
.borders(Borders::default().color(hg))
|
||||||
|
.foreground(fg)
|
||||||
|
.highlighted_color(hg)
|
||||||
|
.title(title, Alignment::Left)
|
||||||
|
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_search(&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::Tab | Key::Up | Key::Down,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Change);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(ch),
|
||||||
|
..
|
||||||
|
}) => match self.perform(Cmd::Type(ch)) {
|
||||||
|
CmdResult::Changed(State::One(StateValue::String(search))) => {
|
||||||
|
Some(Msg::Ui(UiMsg::FuzzySearch(search)))
|
||||||
|
}
|
||||||
|
_ => Some(Msg::None),
|
||||||
|
},
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseFindExplorer))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_file_list(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
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::Char('a'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}) => {
|
||||||
|
let _ = self.perform(Cmd::Custom(file_list::FILE_LIST_CMD_SELECT_ALL));
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('m'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => {
|
||||||
|
let _ = self.perform(Cmd::Toggle);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||||
|
self.perform(Cmd::Change);
|
||||||
|
Some(Msg::None)
|
||||||
|
}
|
||||||
|
// -- comp msg
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Msg::Ui(UiMsg::CloseFindExplorer))
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Left | Key::Right,
|
||||||
|
..
|
||||||
|
}) => Some(Msg::Ui(UiMsg::ChangeTransferWindow)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Enter, ..
|
||||||
|
}) => Some(Msg::Transfer(TransferMsg::EnterDirectory)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char(' '),
|
||||||
|
..
|
||||||
|
}) => Some(Msg::Transfer(TransferMsg::TransferFile)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Backspace,
|
||||||
|
..
|
||||||
|
}) => Some(Msg::Transfer(TransferMsg::GoToPreviousDirectory)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('a'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::ToggleHiddenFiles)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('b'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::ShowFileSortingPopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('e') | Key::Delete | Key::Function(8),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::ShowDeletePopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('i'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('s') | Key::Function(2),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('v') | Key::Function(3),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Transfer(TransferMsg::OpenFile)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('w'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::ShowOpenWithPopup)),
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Char('z'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}) => Some(Msg::Ui(UiMsg::ShowChmodPopup)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<Msg, NoUserEvent> for ExplorerFuzzy {
|
||||||
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
|
match self.component.focus() {
|
||||||
|
file_list_with_search::Focus::List => self.on_file_list(ev),
|
||||||
|
file_list_with_search::Focus::Search => self.on_search(ev),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(MockComponent)]
|
||||||
pub struct ExplorerFind {
|
pub struct ExplorerFind {
|
||||||
@@ -261,7 +467,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
|
|||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('f'),
|
code: Key::Char('f'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
|
}) => Some(Msg::Transfer(TransferMsg::InitFuzzySearch)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('g'),
|
code: Key::Char('g'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
@@ -457,7 +663,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
|
|||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('f'),
|
code: Key::Char('f'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::Ui(UiMsg::ShowFindPopup)),
|
}) => Some(Msg::Transfer(TransferMsg::InitFuzzySearch)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('g'),
|
code: Key::Char('g'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use nucleo::Utf32String;
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
|
|
||||||
use crate::explorer::builder::FileExplorerBuilder;
|
use crate::explorer::builder::FileExplorerBuilder;
|
||||||
use crate::explorer::{FileExplorer, FileSorting, GroupDirs};
|
use crate::explorer::{FileExplorer, FileSorting};
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
|
|
||||||
|
const FUZZY_SEARCH_THRESHOLD: u16 = 50;
|
||||||
|
|
||||||
/// File explorer tab
|
/// File explorer tab
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum FileExplorerTab {
|
pub enum FileExplorerTab {
|
||||||
@@ -28,10 +31,10 @@ pub enum FoundExplorerTab {
|
|||||||
|
|
||||||
/// Browser contains the browser options
|
/// Browser contains the browser options
|
||||||
pub struct Browser {
|
pub struct Browser {
|
||||||
local: FileExplorer, // Local File explorer state
|
local: FileExplorer, // Local File explorer state
|
||||||
remote: FileExplorer, // Remote File explorer state
|
remote: FileExplorer, // Remote File explorer state
|
||||||
found: Option<(FoundExplorerTab, FileExplorer)>, // File explorer for find result
|
found: Option<Found>, // File explorer for find result
|
||||||
tab: FileExplorerTab, // Current selected tab
|
tab: FileExplorerTab, // Current selected tab
|
||||||
pub sync_browsing: bool,
|
pub sync_browsing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,17 +67,35 @@ impl Browser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn found(&self) -> Option<&FileExplorer> {
|
pub fn found(&self) -> Option<&FileExplorer> {
|
||||||
self.found.as_ref().map(|x| &x.1)
|
self.found.as_ref().map(|x| &x.explorer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn found_mut(&mut self) -> Option<&mut FileExplorer> {
|
pub fn found_mut(&mut self) -> Option<&mut FileExplorer> {
|
||||||
self.found.as_mut().map(|x| &mut x.1)
|
self.found.as_mut().map(|x| &mut x.explorer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform fuzzy search on found tab
|
||||||
|
pub fn fuzzy_search(&mut self, needle: &str) {
|
||||||
|
if let Some(x) = self.found.as_mut() {
|
||||||
|
x.fuzzy_search(needle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize fuzzy search
|
||||||
|
pub fn init_fuzzy_search(&mut self) {
|
||||||
|
if let Some(explorer) = self.found_mut() {
|
||||||
|
explorer.set_files(vec![]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec<File>, wrkdir: &Path) {
|
pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec<File>, wrkdir: &Path) {
|
||||||
let mut explorer = Self::build_found_explorer(wrkdir);
|
let mut explorer = Self::build_found_explorer(wrkdir);
|
||||||
explorer.set_files(files);
|
explorer.set_files(files.clone());
|
||||||
self.found = Some((tab, explorer));
|
self.found = Some(Found {
|
||||||
|
tab,
|
||||||
|
explorer,
|
||||||
|
search_results: files,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn del_found(&mut self) {
|
pub fn del_found(&mut self) {
|
||||||
@@ -83,7 +104,7 @@ impl Browser {
|
|||||||
|
|
||||||
/// Returns found tab if any
|
/// Returns found tab if any
|
||||||
pub fn found_tab(&self) -> Option<FoundExplorerTab> {
|
pub fn found_tab(&self) -> Option<FoundExplorerTab> {
|
||||||
self.found.as_ref().map(|x| x.0)
|
self.found.as_ref().map(|x| x.tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab(&self) -> FileExplorerTab {
|
pub fn tab(&self) -> FileExplorerTab {
|
||||||
@@ -129,8 +150,8 @@ impl Browser {
|
|||||||
/// Build explorer reading from `ConfigClient`, for found result (has some differences)
|
/// Build explorer reading from `ConfigClient`, for found result (has some differences)
|
||||||
fn build_found_explorer(wrkdir: &Path) -> FileExplorer {
|
fn build_found_explorer(wrkdir: &Path) -> FileExplorer {
|
||||||
FileExplorerBuilder::new()
|
FileExplorerBuilder::new()
|
||||||
.with_file_sorting(FileSorting::Name)
|
.with_file_sorting(FileSorting::None)
|
||||||
.with_group_dirs(Some(GroupDirs::First))
|
.with_group_dirs(None)
|
||||||
.with_hidden_files(true)
|
.with_hidden_files(true)
|
||||||
.with_stack_size(0)
|
.with_stack_size(0)
|
||||||
.with_formatter(Some(
|
.with_formatter(Some(
|
||||||
@@ -139,3 +160,48 @@ impl Browser {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Found state
|
||||||
|
struct Found {
|
||||||
|
explorer: FileExplorer,
|
||||||
|
/// Search results; original copy of files
|
||||||
|
search_results: Vec<File>,
|
||||||
|
tab: FoundExplorerTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Found {
|
||||||
|
/// Fuzzy search from `search_results` and update `explorer.files` with the results.
|
||||||
|
pub fn fuzzy_search(&mut self, needle: &str) {
|
||||||
|
let search = Utf32String::from(needle);
|
||||||
|
let mut nucleo = nucleo::Matcher::new(nucleo::Config::DEFAULT.match_paths());
|
||||||
|
|
||||||
|
// get scores
|
||||||
|
let mut fuzzy_results_with_score = self
|
||||||
|
.search_results
|
||||||
|
.iter()
|
||||||
|
.map(|f| {
|
||||||
|
(
|
||||||
|
Utf32String::from(f.path().to_string_lossy().into_owned()),
|
||||||
|
f,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter_map(|(path, file)| {
|
||||||
|
nucleo
|
||||||
|
.fuzzy_match(path.slice(..), search.slice(..))
|
||||||
|
.map(|score| (path, file, score))
|
||||||
|
})
|
||||||
|
.filter(|(_, _, score)| *score >= FUZZY_SEARCH_THRESHOLD)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// sort by score; highest first
|
||||||
|
fuzzy_results_with_score.sort_by(|(_, _, a), (_, _, b)| b.cmp(a));
|
||||||
|
|
||||||
|
// update files
|
||||||
|
self.explorer.set_files(
|
||||||
|
fuzzy_results_with_score
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, file, _)| file.clone())
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ enum Id {
|
|||||||
FatalPopup,
|
FatalPopup,
|
||||||
FileInfoPopup,
|
FileInfoPopup,
|
||||||
FilterPopup,
|
FilterPopup,
|
||||||
FindPopup,
|
|
||||||
FooterBar,
|
FooterBar,
|
||||||
GlobalListener,
|
GlobalListener,
|
||||||
GotoPopup,
|
GotoPopup,
|
||||||
@@ -104,6 +103,7 @@ enum TransferMsg {
|
|||||||
GoTo(String),
|
GoTo(String),
|
||||||
GoToParentDirectory,
|
GoToParentDirectory,
|
||||||
GoToPreviousDirectory,
|
GoToPreviousDirectory,
|
||||||
|
InitFuzzySearch,
|
||||||
Mkdir(String),
|
Mkdir(String),
|
||||||
NewFile(String),
|
NewFile(String),
|
||||||
OpenFile,
|
OpenFile,
|
||||||
@@ -112,7 +112,6 @@ enum TransferMsg {
|
|||||||
ReloadDir,
|
ReloadDir,
|
||||||
RenameFile(String),
|
RenameFile(String),
|
||||||
SaveFileAs(String),
|
SaveFileAs(String),
|
||||||
SearchFile(String),
|
|
||||||
ToggleWatch,
|
ToggleWatch,
|
||||||
ToggleWatchFor(usize),
|
ToggleWatchFor(usize),
|
||||||
TransferFile,
|
TransferFile,
|
||||||
@@ -133,7 +132,6 @@ enum UiMsg {
|
|||||||
CloseFileSortingPopup,
|
CloseFileSortingPopup,
|
||||||
CloseFilterPopup,
|
CloseFilterPopup,
|
||||||
CloseFindExplorer,
|
CloseFindExplorer,
|
||||||
CloseFindPopup,
|
|
||||||
CloseGotoPopup,
|
CloseGotoPopup,
|
||||||
CloseKeybindingsPopup,
|
CloseKeybindingsPopup,
|
||||||
CloseMkdirPopup,
|
CloseMkdirPopup,
|
||||||
@@ -147,6 +145,7 @@ enum UiMsg {
|
|||||||
CloseWatcherPopup,
|
CloseWatcherPopup,
|
||||||
Disconnect,
|
Disconnect,
|
||||||
FilterFiles(String),
|
FilterFiles(String),
|
||||||
|
FuzzySearch(String),
|
||||||
LogBackTabbed,
|
LogBackTabbed,
|
||||||
Quit,
|
Quit,
|
||||||
ReplacePopupTabbed,
|
ReplacePopupTabbed,
|
||||||
@@ -158,7 +157,6 @@ enum UiMsg {
|
|||||||
ShowFileInfoPopup,
|
ShowFileInfoPopup,
|
||||||
ShowFileSortingPopup,
|
ShowFileSortingPopup,
|
||||||
ShowFilterPopup,
|
ShowFilterPopup,
|
||||||
ShowFindPopup,
|
|
||||||
ShowGotoPopup,
|
ShowGotoPopup,
|
||||||
ShowKeybindingsPopup,
|
ShowKeybindingsPopup,
|
||||||
ShowLogPanel,
|
ShowLogPanel,
|
||||||
|
|||||||
@@ -211,6 +211,56 @@ impl FileTransferActivity {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TransferMsg::InitFuzzySearch => {
|
||||||
|
// Mount wait
|
||||||
|
self.mount_blocking_wait("Scanning current directory…");
|
||||||
|
// Find
|
||||||
|
let res: Result<Vec<File>, String> = match self.browser.tab() {
|
||||||
|
FileExplorerTab::Local => self.action_walkdir_local(),
|
||||||
|
FileExplorerTab::Remote => self.action_walkdir_remote(),
|
||||||
|
_ => panic!("Trying to search for files, while already in a find result"),
|
||||||
|
};
|
||||||
|
// Umount wait
|
||||||
|
self.umount_wait();
|
||||||
|
// Match result
|
||||||
|
match res {
|
||||||
|
Err(err) => {
|
||||||
|
// Mount error
|
||||||
|
self.mount_error(err.as_str());
|
||||||
|
}
|
||||||
|
Ok(files) if files.is_empty() => {
|
||||||
|
// If no file has been found notify user
|
||||||
|
self.mount_info("There are no files in the current directory");
|
||||||
|
}
|
||||||
|
Ok(files) => {
|
||||||
|
// 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(),
|
||||||
|
);
|
||||||
|
// init fuzzy search to display nothing
|
||||||
|
self.browser.init_fuzzy_search();
|
||||||
|
// Mount result widget
|
||||||
|
self.mount_find(format!(r#"Searching at "{}""#, wrkdir.display()), true);
|
||||||
|
self.update_find_list();
|
||||||
|
// Initialize tab
|
||||||
|
self.browser.change_tab(match self.browser.tab() {
|
||||||
|
FileExplorerTab::Local => FileExplorerTab::FindLocal,
|
||||||
|
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
|
||||||
|
_ => FileExplorerTab::FindLocal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
TransferMsg::Mkdir(dir) => {
|
TransferMsg::Mkdir(dir) => {
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_local_mkdir(dir),
|
FileExplorerTab::Local => self.action_local_mkdir(dir),
|
||||||
@@ -281,57 +331,7 @@ impl FileTransferActivity {
|
|||||||
// Reload files
|
// Reload files
|
||||||
self.update_browser_file_list_swapped();
|
self.update_browser_file_list_swapped();
|
||||||
}
|
}
|
||||||
TransferMsg::SearchFile(search) => {
|
|
||||||
self.umount_find_input();
|
|
||||||
// Mount wait
|
|
||||||
self.mount_blocking_wait(format!(r#"Searching for "{search}"…"#).as_str());
|
|
||||||
// Find
|
|
||||||
let res: Result<Vec<File>, String> = match self.browser.tab() {
|
|
||||||
FileExplorerTab::Local => self.action_local_find(search.clone()),
|
|
||||||
FileExplorerTab::Remote => self.action_remote_find(search.clone()),
|
|
||||||
_ => panic!("Trying to search for files, while already in a find result"),
|
|
||||||
};
|
|
||||||
// Umount wait
|
|
||||||
self.umount_wait();
|
|
||||||
// Match result
|
|
||||||
match res {
|
|
||||||
Err(err) => {
|
|
||||||
// Mount error
|
|
||||||
self.mount_error(err.as_str());
|
|
||||||
}
|
|
||||||
Ok(files) if files.is_empty() => {
|
|
||||||
// If no file has been found notify user
|
|
||||||
self.mount_info(
|
|
||||||
format!(r#"Could not find any file matching "{search}""#).as_str(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(files) => {
|
|
||||||
// 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(&search);
|
|
||||||
self.update_find_list();
|
|
||||||
// Initialize tab
|
|
||||||
self.browser.change_tab(match self.browser.tab() {
|
|
||||||
FileExplorerTab::Local => FileExplorerTab::FindLocal,
|
|
||||||
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
|
|
||||||
_ => FileExplorerTab::FindLocal,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TransferMsg::ToggleWatch => self.action_toggle_watch(),
|
TransferMsg::ToggleWatch => self.action_toggle_watch(),
|
||||||
TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index),
|
TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index),
|
||||||
TransferMsg::TransferFile => {
|
TransferMsg::TransferFile => {
|
||||||
@@ -405,7 +405,6 @@ impl FileTransferActivity {
|
|||||||
self.finalize_find();
|
self.finalize_find();
|
||||||
self.umount_find();
|
self.umount_find();
|
||||||
}
|
}
|
||||||
UiMsg::CloseFindPopup => self.umount_find_input(),
|
|
||||||
UiMsg::CloseGotoPopup => self.umount_goto(),
|
UiMsg::CloseGotoPopup => self.umount_goto(),
|
||||||
UiMsg::CloseKeybindingsPopup => self.umount_help(),
|
UiMsg::CloseKeybindingsPopup => self.umount_help(),
|
||||||
UiMsg::CloseMkdirPopup => self.umount_mkdir(),
|
UiMsg::CloseMkdirPopup => self.umount_mkdir(),
|
||||||
@@ -439,7 +438,7 @@ impl FileTransferActivity {
|
|||||||
wrkdir.as_path(),
|
wrkdir.as_path(),
|
||||||
);
|
);
|
||||||
// Mount result widget
|
// Mount result widget
|
||||||
self.mount_find(&filter);
|
self.mount_find(&filter, false);
|
||||||
self.update_find_list();
|
self.update_find_list();
|
||||||
// Initialize tab
|
// Initialize tab
|
||||||
self.browser.change_tab(match self.browser.tab() {
|
self.browser.change_tab(match self.browser.tab() {
|
||||||
@@ -448,6 +447,10 @@ impl FileTransferActivity {
|
|||||||
_ => FileExplorerTab::FindLocal,
|
_ => FileExplorerTab::FindLocal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
UiMsg::FuzzySearch(needle) => {
|
||||||
|
self.browser.fuzzy_search(&needle);
|
||||||
|
self.update_find_list();
|
||||||
|
}
|
||||||
UiMsg::ShowLogPanel => {
|
UiMsg::ShowLogPanel => {
|
||||||
assert!(self.app.active(&Id::Log).is_ok());
|
assert!(self.app.active(&Id::Log).is_ok());
|
||||||
}
|
}
|
||||||
@@ -514,7 +517,6 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
UiMsg::ShowFileSortingPopup => self.mount_file_sorting(),
|
UiMsg::ShowFileSortingPopup => self.mount_file_sorting(),
|
||||||
UiMsg::ShowFilterPopup => self.mount_filter(),
|
UiMsg::ShowFilterPopup => self.mount_filter(),
|
||||||
UiMsg::ShowFindPopup => self.mount_find_input(),
|
|
||||||
UiMsg::ShowGotoPopup => self.mount_goto(),
|
UiMsg::ShowGotoPopup => self.mount_goto(),
|
||||||
UiMsg::ShowKeybindingsPopup => self.mount_help(),
|
UiMsg::ShowKeybindingsPopup => self.mount_help(),
|
||||||
UiMsg::ShowMkdirPopup => self.mount_mkdir(),
|
UiMsg::ShowMkdirPopup => self.mount_mkdir(),
|
||||||
|
|||||||
@@ -177,11 +177,6 @@ impl FileTransferActivity {
|
|||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
// make popup
|
// make popup
|
||||||
self.app.view(&Id::FilterPopup, f, 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);
|
|
||||||
// make popup
|
|
||||||
self.app.view(&Id::FindPopup, f, popup);
|
|
||||||
} else if self.app.mounted(&Id::GotoPopup) {
|
} else if self.app.mounted(&Id::GotoPopup) {
|
||||||
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size());
|
let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.size());
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
@@ -515,7 +510,7 @@ impl FileTransferActivity {
|
|||||||
let _ = self.app.umount(&Id::ExecPopup);
|
let _ = self.app.umount(&Id::ExecPopup);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_find(&mut self, search: &str) {
|
pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) {
|
||||||
// Get color
|
// Get color
|
||||||
let (bg, fg, hg) = match self.browser.tab() {
|
let (bg, fg, hg) = match self.browser.tab() {
|
||||||
FileExplorerTab::Local | FileExplorerTab::FindLocal => (
|
FileExplorerTab::Local | FileExplorerTab::FindLocal => (
|
||||||
@@ -529,18 +524,29 @@ impl FileTransferActivity {
|
|||||||
self.theme().transfer_remote_explorer_highlighted,
|
self.theme().transfer_remote_explorer_highlighted,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mount component
|
// Mount component
|
||||||
assert!(self
|
assert!(self
|
||||||
.app
|
.app
|
||||||
.remount(
|
.remount(
|
||||||
Id::ExplorerFind,
|
Id::ExplorerFind,
|
||||||
Box::new(components::ExplorerFind::new(
|
if fuzzy_search {
|
||||||
format!(r#"Search results for "{search}""#),
|
Box::new(components::ExplorerFuzzy::new(
|
||||||
&[],
|
msg.to_string(),
|
||||||
bg,
|
&[],
|
||||||
fg,
|
bg,
|
||||||
hg
|
fg,
|
||||||
)),
|
hg,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Box::new(components::ExplorerFind::new(
|
||||||
|
msg.to_string(),
|
||||||
|
&[],
|
||||||
|
bg,
|
||||||
|
fg,
|
||||||
|
hg,
|
||||||
|
))
|
||||||
|
},
|
||||||
vec![],
|
vec![],
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
@@ -551,24 +557,6 @@ impl FileTransferActivity {
|
|||||||
let _ = self.app.umount(&Id::ExplorerFind);
|
let _ = self.app.umount(&Id::ExplorerFind);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_find_input(&mut self) {
|
|
||||||
let input_color = self.theme().misc_input_dialog;
|
|
||||||
assert!(self
|
|
||||||
.app
|
|
||||||
.remount(
|
|
||||||
Id::FindPopup,
|
|
||||||
Box::new(components::FindPopup::new(input_color)),
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.is_ok());
|
|
||||||
assert!(self.app.active(&Id::FindPopup).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn umount_find_input(&mut self) {
|
|
||||||
// Umount input find
|
|
||||||
let _ = self.app.umount(&Id::FindPopup);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn mount_goto(&mut self) {
|
pub(super) fn mount_goto(&mut self) {
|
||||||
let input_color = self.theme().misc_input_dialog;
|
let input_color = self.theme().misc_input_dialog;
|
||||||
assert!(self
|
assert!(self
|
||||||
@@ -1094,38 +1082,33 @@ impl FileTransferActivity {
|
|||||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||||
Id::SortingPopup,
|
Id::SortingPopup,
|
||||||
)))),
|
)))),
|
||||||
Box::new(SubClause::And(
|
|
||||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
|
||||||
Id::FindPopup,
|
|
||||||
)))),
|
|
||||||
Box::new(SubClause::And(
|
Box::new(SubClause::And(
|
||||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||||
Id::SyncBrowsingMkdirPopup,
|
Id::SyncBrowsingMkdirPopup,
|
||||||
|
)))),
|
||||||
|
Box::new(SubClause::And(
|
||||||
|
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||||
|
Id::SymlinkPopup,
|
||||||
)))),
|
)))),
|
||||||
Box::new(SubClause::And(
|
Box::new(SubClause::And(
|
||||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||||
Id::SymlinkPopup,
|
Id::WatcherPopup,
|
||||||
)))),
|
)))),
|
||||||
Box::new(SubClause::And(
|
Box::new(SubClause::And(
|
||||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||||
Id::WatcherPopup,
|
Id::WatchedPathsList,
|
||||||
)))),
|
)))),
|
||||||
Box::new(SubClause::And(
|
Box::new(SubClause::And(
|
||||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||||
Id::WatchedPathsList,
|
Id::ChmodPopup,
|
||||||
)))),
|
)))),
|
||||||
Box::new(SubClause::And(
|
Box::new(SubClause::And(
|
||||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||||
Id::ChmodPopup,
|
Id::WaitPopup,
|
||||||
|
)))),
|
||||||
|
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||||
|
Id::FilterPopup,
|
||||||
)))),
|
)))),
|
||||||
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