mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: find (#104)
This commit is contained in:
parent
0acf345ee3
commit
7ec7e36a53
23 changed files with 403 additions and 64 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -177,9 +177,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.3"
|
version = "0.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
|
checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
|
|
@ -1548,9 +1548,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.105"
|
version = "1.0.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
|
checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
|
@ -1880,9 +1880,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.7.7"
|
version = "0.7.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de0a3ab2091e52d7299a39d098e200114a972df0a7724add02a273aa9aada592"
|
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.0.0",
|
"indexmap 2.0.0",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use core::{emit, files::FilesOp, input::InputMode, Event};
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
|
||||||
use anyhow::{Ok, Result};
|
use anyhow::{Ok, Result};
|
||||||
use config::{keymap::{Control, Key, KeymapLayer}, BOOT};
|
use config::{keymap::{Exec, Key, KeymapLayer}, BOOT};
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use shared::{expand_url, Term};
|
use shared::{expand_url, Term};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
@ -34,7 +34,7 @@ impl App {
|
||||||
Event::Render(_) => app.dispatch_render(),
|
Event::Render(_) => app.dispatch_render(),
|
||||||
Event::Resize(..) => app.dispatch_resize(),
|
Event::Resize(..) => app.dispatch_resize(),
|
||||||
Event::Stop(state, tx) => app.dispatch_stop(state, tx),
|
Event::Stop(state, tx) => app.dispatch_stop(state, tx),
|
||||||
Event::Ctrl(ctrl, layer) => app.dispatch_ctrl(ctrl, layer),
|
Event::Call(exec, layer) => app.dispatch_call(exec, layer),
|
||||||
event => app.dispatch_module(event),
|
event => app.dispatch_module(event),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,8 +109,8 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn dispatch_ctrl(&mut self, ctrl: Control, layer: KeymapLayer) {
|
fn dispatch_call(&mut self, exec: Vec<Exec>, layer: KeymapLayer) {
|
||||||
if Executor::dispatch(&mut self.cx, &ctrl.exec, layer) {
|
if Executor::dispatch(&mut self.cx, &exec, layer) {
|
||||||
emit!(Render);
|
emit!(Render);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ impl Executor {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn dispatch(cx: &mut Ctx, exec: &Vec<Exec>, layer: KeymapLayer) -> bool {
|
pub(super) fn dispatch(cx: &mut Ctx, exec: &[Exec], layer: KeymapLayer) -> bool {
|
||||||
let mut render = false;
|
let mut render = false;
|
||||||
for e in exec {
|
for e in exec {
|
||||||
render |= match layer {
|
render |= match layer {
|
||||||
|
|
@ -138,6 +138,14 @@ impl Executor {
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Find
|
||||||
|
"find" => {
|
||||||
|
let query = exec.args.get(0).map(|s| s.as_str());
|
||||||
|
let prev = exec.named.contains_key("previous");
|
||||||
|
cx.manager.active_mut().find(query, prev)
|
||||||
|
}
|
||||||
|
"find_arrow" => cx.manager.active_mut().find_arrow(exec.named.contains_key("previous")),
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
"sort" => {
|
"sort" => {
|
||||||
let b = cx.manager.active_mut().set_sorter(FilesSorter {
|
let b = cx.manager.active_mut().set_sorter(FilesSorter {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use core::files::File;
|
use core::files::File;
|
||||||
|
|
||||||
use config::{MANAGER, THEME};
|
use config::{MANAGER, THEME};
|
||||||
use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::{List, ListItem, Widget}};
|
use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{List, ListItem, Widget}};
|
||||||
use shared::short_path;
|
use shared::short_path;
|
||||||
|
|
||||||
use crate::Ctx;
|
use crate::Ctx;
|
||||||
|
|
@ -11,11 +11,12 @@ pub(super) struct Folder<'a> {
|
||||||
folder: &'a core::manager::Folder,
|
folder: &'a core::manager::Folder,
|
||||||
is_preview: bool,
|
is_preview: bool,
|
||||||
is_selection: bool,
|
is_selection: bool,
|
||||||
|
is_find: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Folder<'a> {
|
impl<'a> Folder<'a> {
|
||||||
pub(super) fn new(cx: &'a Ctx, folder: &'a core::manager::Folder) -> Self {
|
pub(super) fn new(cx: &'a Ctx, folder: &'a core::manager::Folder) -> Self {
|
||||||
Self { cx, folder, is_preview: false, is_selection: false }
|
Self { cx, folder, is_preview: false, is_selection: false, is_find: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -30,6 +31,24 @@ impl<'a> Folder<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn with_find(mut self, state: bool) -> Self {
|
||||||
|
self.is_find = state;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Folder<'a> {
|
||||||
|
#[inline]
|
||||||
|
fn icon(file: &File) -> &'static str {
|
||||||
|
THEME
|
||||||
|
.icons
|
||||||
|
.iter()
|
||||||
|
.find(|x| x.name.match_path(file.url(), Some(file.is_dir())))
|
||||||
|
.map(|x| x.display.as_ref())
|
||||||
|
.unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn file_style(&self, file: &File) -> Style {
|
fn file_style(&self, file: &File) -> Style {
|
||||||
let mimetype = &self.cx.manager.mimetype;
|
let mimetype = &self.cx.manager.mimetype;
|
||||||
|
|
@ -53,17 +72,10 @@ impl<'a> Widget for Folder<'a> {
|
||||||
self.folder.window()
|
self.folder.window()
|
||||||
};
|
};
|
||||||
|
|
||||||
let items = window
|
let items: Vec<_> = window
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, f)| {
|
.map(|(i, f)| {
|
||||||
let icon = THEME
|
|
||||||
.icons
|
|
||||||
.iter()
|
|
||||||
.find(|x| x.name.match_path(f.url(), Some(f.is_dir())))
|
|
||||||
.map(|x| x.display.as_ref())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
let is_selected = self.folder.files.is_selected(f.url());
|
let is_selected = self.folder.files.is_selected(f.url());
|
||||||
if (!self.is_selection && is_selected)
|
if (!self.is_selection && is_selected)
|
||||||
|| (self.is_selection && mode.pending(self.folder.offset() + i, is_selected))
|
|| (self.is_selection && mode.pending(self.folder.offset() + i, is_selected))
|
||||||
|
|
@ -87,16 +99,37 @@ impl<'a> Widget for Folder<'a> {
|
||||||
self.file_style(f)
|
self.file_style(f)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut path = format!(" {icon} {}", short_path(f.url(), &self.folder.cwd));
|
let mut spans = Vec::with_capacity(10);
|
||||||
|
|
||||||
|
spans.push(Span::raw(format!(" {} ", Self::icon(f))));
|
||||||
|
spans.push(Span::raw(short_path(f.url(), &self.folder.cwd)));
|
||||||
|
|
||||||
if let Some(link_to) = f.link_to() {
|
if let Some(link_to) = f.link_to() {
|
||||||
if MANAGER.show_symlink {
|
if MANAGER.show_symlink {
|
||||||
path.push_str(&format!(" -> {}", link_to.display()));
|
spans.push(Span::raw(format!(" -> {}", link_to.display())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListItem::new(path).style(style)
|
if let Some(idx) = active
|
||||||
|
.finder()
|
||||||
|
.filter(|&f| hovered && self.is_find && f.has_matched())
|
||||||
|
.and_then(|finder| finder.matched_idx(f.url()))
|
||||||
|
{
|
||||||
|
let len = active.finder().unwrap().matched().len();
|
||||||
|
let style = Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC);
|
||||||
|
spans.push(Span::styled(
|
||||||
|
format!(
|
||||||
|
" [{}/{}]",
|
||||||
|
if idx > 99 { ">99".to_string() } else { (idx + 1).to_string() },
|
||||||
|
if len > 99 { ">99".to_string() } else { len.to_string() }
|
||||||
|
),
|
||||||
|
style,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItem::new(Line::from(spans)).style(style)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect();
|
||||||
|
|
||||||
List::new(items).render(area, buf);
|
List::new(items).render(area, buf);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ impl<'a> Widget for Layout<'a> {
|
||||||
// Current
|
// Current
|
||||||
Folder::new(self.cx, manager.current())
|
Folder::new(self.cx, manager.current())
|
||||||
.with_selection(manager.active().mode().is_visual())
|
.with_selection(manager.active().mode().is_visual())
|
||||||
|
.with_find(manager.active().finder().is_some())
|
||||||
.render(chunks[1], buf);
|
.render(chunks[1], buf);
|
||||||
|
|
||||||
// Preview
|
// Preview
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ impl<'a> Widget for Left<'a> {
|
||||||
let separator = &THEME.status.separator;
|
let separator = &THEME.status.separator;
|
||||||
|
|
||||||
// Mode
|
// Mode
|
||||||
let mut spans = vec![];
|
let mut spans = Vec::with_capacity(5);
|
||||||
spans.push(Span::styled(&separator.opening, primary.fg()));
|
spans.push(Span::styled(&separator.opening, primary.fg()));
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
format!(" {mode} "),
|
format!(" {mode} "),
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ impl<'a> Right<'a> {
|
||||||
impl Widget for Right<'_> {
|
impl Widget for Right<'_> {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let manager = self.cx.manager.current();
|
let manager = self.cx.manager.current();
|
||||||
let mut spans = vec![];
|
let mut spans = Vec::with_capacity(20);
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ impl Widget for Side<'_> {
|
||||||
.cands
|
.cands
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
let mut spans = vec![];
|
let mut spans = Vec::with_capacity(10);
|
||||||
|
|
||||||
// Keys
|
// Keys
|
||||||
let keys = c.on[self.times..].iter().map(ToString::to_string).collect::<Vec<_>>();
|
let keys = c.on[self.times..].iter().map(ToString::to_string).collect::<Vec<_>>();
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,12 @@ keymap = [
|
||||||
{ on = [ "c", "f" ], exec = "copy filename", desc = "Copy the name of the file" },
|
{ on = [ "c", "f" ], exec = "copy filename", desc = "Copy the name of the file" },
|
||||||
{ on = [ "c", "n" ], exec = "copy name_without_ext", desc = "Copy the name of the file without the extension" },
|
{ on = [ "c", "n" ], exec = "copy name_without_ext", desc = "Copy the name of the file without the extension" },
|
||||||
|
|
||||||
|
# Find
|
||||||
|
{ on = [ "/" ], exec = "find" },
|
||||||
|
{ on = [ "?" ], exec = "find --previous" },
|
||||||
|
{ on = [ "-" ], exec = "find_arrow" },
|
||||||
|
{ on = [ "=" ], exec = "find_arrow --previous" },
|
||||||
|
|
||||||
# Sorting
|
# Sorting
|
||||||
{ on = [ ",", "a" ], exec = "sort alphabetical --dir_first", desc = "Sort alphabetically, directories first" },
|
{ on = [ ",", "a" ], exec = "sort alphabetical --dir_first", desc = "Sort alphabetically, directories first" },
|
||||||
{ on = [ ",", "A" ], exec = "sort alphabetical --reverse --dir_first", desc = "Sort alphabetically, directories first (reverse)" },
|
{ on = [ ",", "A" ], exec = "sort alphabetical --reverse --dir_first", desc = "Sort alphabetically, directories first (reverse)" },
|
||||||
|
|
@ -106,7 +112,7 @@ keymap = [
|
||||||
{ on = [ "g", "<Space>" ], exec = "cd --interactive", desc = "Go to a directory interactively" },
|
{ on = [ "g", "<Space>" ], exec = "cd --interactive", desc = "Go to a directory interactively" },
|
||||||
|
|
||||||
# Help
|
# Help
|
||||||
{ on = [ "?" ], exec = "help", desc = "Open help" },
|
{ on = [ "~" ], exec = "help", desc = "Open help" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[tasks]
|
[tasks]
|
||||||
|
|
@ -125,7 +131,7 @@ keymap = [
|
||||||
{ on = [ "<Enter>" ], exec = "inspect", desc = "Inspect the task" },
|
{ on = [ "<Enter>" ], exec = "inspect", desc = "Inspect the task" },
|
||||||
{ on = [ "x" ], exec = "cancel", desc = "Cancel the task" },
|
{ on = [ "x" ], exec = "cancel", desc = "Cancel the task" },
|
||||||
|
|
||||||
{ on = [ "?" ], exec = "help", desc = "Open help" }
|
{ on = [ "~" ], exec = "help", desc = "Open help" }
|
||||||
]
|
]
|
||||||
|
|
||||||
[select]
|
[select]
|
||||||
|
|
@ -144,7 +150,7 @@ keymap = [
|
||||||
{ on = [ "<Up>" ], exec = "arrow -1", desc = "Move cursor up" },
|
{ on = [ "<Up>" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||||
{ on = [ "<Down>" ], exec = "arrow 1", desc = "Move cursor down" },
|
{ on = [ "<Down>" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||||
|
|
||||||
{ on = [ "?" ], exec = "help", desc = "Open help" }
|
{ on = [ "~" ], exec = "help", desc = "Open help" }
|
||||||
]
|
]
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
@ -190,7 +196,7 @@ keymap = [
|
||||||
{ on = [ "<C-r>" ], exec = "redo", desc = "Redo the last operation" },
|
{ on = [ "<C-r>" ], exec = "redo", desc = "Redo the last operation" },
|
||||||
|
|
||||||
# Help
|
# Help
|
||||||
{ on = [ "?" ], exec = "help", desc = "Open help" }
|
{ on = [ "~" ], exec = "help", desc = "Open help" }
|
||||||
]
|
]
|
||||||
|
|
||||||
[help]
|
[help]
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,11 @@ pub struct Control {
|
||||||
pub desc: Option<String>,
|
pub desc: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Control {
|
||||||
|
#[inline]
|
||||||
|
pub fn to_call(&self) -> Vec<Exec> { self.exec.clone() }
|
||||||
|
}
|
||||||
|
|
||||||
impl Control {
|
impl Control {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() }
|
pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() }
|
||||||
|
|
@ -28,7 +33,7 @@ impl Control {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn contains(&self, s: &str) -> bool {
|
pub fn contains(&self, s: &str) -> bool {
|
||||||
self.desc.as_ref().map(|d| d.contains(s)).unwrap_or(false)
|
self.desc.as_ref().map(|d| d.contains(s)) == Some(true)
|
||||||
|| self.exec().contains(s)
|
|| self.exec().contains(s)
|
||||||
|| self.on().contains(s)
|
|| self.on().contains(s)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,3 +86,26 @@ impl Exec {
|
||||||
deserializer.deserialize_any(ExecVisitor)
|
deserializer.deserialize_any(ExecVisitor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Exec {
|
||||||
|
#[inline]
|
||||||
|
pub fn call(cwd: &str, args: Vec<String>) -> Self {
|
||||||
|
Exec { cmd: cwd.to_owned(), args, named: Default::default() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn call_named(cwd: &str, named: BTreeMap<String, String>) -> Self {
|
||||||
|
Exec { cmd: cwd.to_owned(), args: Default::default(), named }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn vec(self) -> Vec<Self> { vec![self] }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn with_bool(mut self, name: &str, state: bool) -> Self {
|
||||||
|
if state {
|
||||||
|
self.named.insert(name.to_string(), "".to_string());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{collections::BTreeMap, ffi::OsString};
|
use std::{collections::BTreeMap, ffi::OsString};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use config::{keymap::{Control, KeymapLayer}, open::Opener};
|
use config::{keymap::{Exec, KeymapLayer}, open::Opener};
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use shared::{InputError, RoCell, Url};
|
use shared::{InputError, RoCell, Url};
|
||||||
use tokio::sync::{mpsc::{self, UnboundedSender}, oneshot};
|
use tokio::sync::{mpsc::{self, UnboundedSender}, oneshot};
|
||||||
|
|
@ -18,7 +18,7 @@ pub enum Event {
|
||||||
Render(String),
|
Render(String),
|
||||||
Resize(u16, u16),
|
Resize(u16, u16),
|
||||||
Stop(bool, Option<oneshot::Sender<()>>),
|
Stop(bool, Option<oneshot::Sender<()>>),
|
||||||
Ctrl(Control, KeymapLayer),
|
Call(Vec<Exec>, KeymapLayer),
|
||||||
|
|
||||||
// Manager
|
// Manager
|
||||||
Cd(Url),
|
Cd(Url),
|
||||||
|
|
@ -67,8 +67,8 @@ macro_rules! emit {
|
||||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
$crate::Event::Stop($state, Some(tx)).wait(rx)
|
$crate::Event::Stop($state, Some(tx)).wait(rx)
|
||||||
}};
|
}};
|
||||||
(Ctrl($exec:expr, $layer:expr)) => {
|
(Call($exec:expr, $layer:expr)) => {
|
||||||
$crate::Event::Ctrl($exec, $layer).emit();
|
$crate::Event::Call($exec, $layer).emit();
|
||||||
};
|
};
|
||||||
|
|
||||||
(Cd($url:expr)) => {
|
(Cd($url:expr)) => {
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,12 @@ use config::manager::SortBy;
|
||||||
use shared::Url;
|
use shared::Url;
|
||||||
use tokio::{fs, select, sync::mpsc::{self, UnboundedReceiver}};
|
use tokio::{fs, select, sync::mpsc::{self, UnboundedReceiver}};
|
||||||
|
|
||||||
use super::{File, FilesSorter, FILES_VERSION};
|
use super::{File, FilesSorter, FILES_TICKET};
|
||||||
|
|
||||||
pub struct Files {
|
pub struct Files {
|
||||||
items: Vec<File>,
|
items: Vec<File>,
|
||||||
hidden: Vec<File>,
|
hidden: Vec<File>,
|
||||||
|
ticket: u64,
|
||||||
version: u64,
|
version: u64,
|
||||||
|
|
||||||
sizes: BTreeMap<Url, u64>,
|
sizes: BTreeMap<Url, u64>,
|
||||||
|
|
@ -24,6 +25,7 @@ impl Default for Files {
|
||||||
Self {
|
Self {
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
hidden: Default::default(),
|
hidden: Default::default(),
|
||||||
|
ticket: Default::default(),
|
||||||
version: Default::default(),
|
version: Default::default(),
|
||||||
|
|
||||||
sizes: Default::default(),
|
sizes: Default::default(),
|
||||||
|
|
@ -126,15 +128,16 @@ impl Files {
|
||||||
if !self.show_hidden {
|
if !self.show_hidden {
|
||||||
(self.hidden, items) = items.into_iter().partition(|f| f.is_hidden);
|
(self.hidden, items) = items.into_iter().partition(|f| f.is_hidden);
|
||||||
}
|
}
|
||||||
|
self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed);
|
||||||
self.sorter.sort(&mut items, &self.sizes);
|
self.sorter.sort(&mut items, &self.sizes);
|
||||||
self.items = items;
|
self.items = items;
|
||||||
self.version = FILES_VERSION.fetch_add(1, Ordering::Relaxed);
|
self.version += 1;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_part(&mut self, version: u64, items: Vec<File>) -> bool {
|
pub fn update_part(&mut self, version: u64, items: Vec<File>) -> bool {
|
||||||
if !items.is_empty() {
|
if !items.is_empty() {
|
||||||
if version != self.version {
|
if version != self.ticket {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,22 +150,26 @@ impl Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sorter.sort(&mut self.items, &self.sizes);
|
self.sorter.sort(&mut self.items, &self.sizes);
|
||||||
|
self.version += 1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.version = version;
|
self.ticket = version;
|
||||||
if !self.items.is_empty() {
|
if self.items.is_empty() && self.hidden.is_empty() {
|
||||||
self.items.clear();
|
return false;
|
||||||
self.hidden.clear();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
false
|
|
||||||
|
self.items.clear();
|
||||||
|
self.hidden.clear();
|
||||||
|
self.version += 1;
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_size(&mut self, items: BTreeMap<Url, u64>) -> bool {
|
pub fn update_size(&mut self, items: BTreeMap<Url, u64>) -> bool {
|
||||||
self.sizes.extend(items);
|
self.sizes.extend(items);
|
||||||
if self.sorter.by == SortBy::Size {
|
if self.sorter.by == SortBy::Size {
|
||||||
self.sorter.sort(&mut self.items, &self.sizes);
|
self.sorter.sort(&mut self.items, &self.sizes);
|
||||||
|
self.version += 1;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +193,11 @@ impl Files {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn duplicate(&self, idx: usize) -> Option<File> { self.items.get(idx).cloned() }
|
pub fn duplicate(&self, idx: usize) -> Option<File> { self.items.get(idx).cloned() }
|
||||||
|
|
||||||
// --- Size
|
// --- Version
|
||||||
|
#[inline]
|
||||||
|
pub fn version(&self) -> u64 { self.version }
|
||||||
|
|
||||||
|
// --- Sizes
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn size(&self, url: &Url) -> Option<u64> { self.sizes.get(url).copied() }
|
pub fn size(&self, url: &Url) -> Option<u64> { self.sizes.get(url).copied() }
|
||||||
|
|
||||||
|
|
@ -230,6 +241,7 @@ impl Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Sorter
|
// --- Sorter
|
||||||
|
#[inline]
|
||||||
pub fn sorter(&self) -> &FilesSorter { &self.sorter }
|
pub fn sorter(&self) -> &FilesSorter { &self.sorter }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -238,6 +250,7 @@ impl Files {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.sorter = sorter;
|
self.sorter = sorter;
|
||||||
|
self.version += 1;
|
||||||
self.sorter.sort(&mut self.items, &self.sizes)
|
self.sorter.sort(&mut self.items, &self.sizes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,6 +272,7 @@ impl Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.show_hidden = state;
|
self.show_hidden = state;
|
||||||
|
self.version += 1;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use shared::Url;
|
||||||
use super::File;
|
use super::File;
|
||||||
use crate::emit;
|
use crate::emit;
|
||||||
|
|
||||||
pub(super) static FILES_VERSION: AtomicU64 = AtomicU64::new(0);
|
pub(super) static FILES_TICKET: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FilesOp {
|
pub enum FilesOp {
|
||||||
|
|
@ -28,8 +28,8 @@ impl FilesOp {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn prepare(url: &Url) -> u64 {
|
pub fn prepare(url: &Url) -> u64 {
|
||||||
let version = FILES_VERSION.fetch_add(1, Ordering::Relaxed);
|
let ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed);
|
||||||
emit!(Files(Self::Part(url.clone(), version, Vec::new())));
|
emit!(Files(Self::Part(url.clone(), ticket, Vec::new())));
|
||||||
version
|
ticket
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
core/src/manager/finder.rs
Normal file
130
core/src/manager/finder.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
use std::{collections::BTreeMap, ffi::OsStr};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use regex::bytes::Regex;
|
||||||
|
use shared::Url;
|
||||||
|
|
||||||
|
use crate::files::Files;
|
||||||
|
|
||||||
|
pub struct Finder {
|
||||||
|
query: Regex,
|
||||||
|
matched: BTreeMap<Url, u8>,
|
||||||
|
version: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Finder {
|
||||||
|
pub(super) fn new(s: &str) -> Result<Self> {
|
||||||
|
Ok(Self { query: Regex::new(s)?, matched: Default::default(), version: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn arrow(&self, files: &Files, cursor: usize, prev: bool) -> Option<isize> {
|
||||||
|
if prev {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.take(cursor)
|
||||||
|
.rev()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, f)| f.name().map_or(false, |n| self.matches(n)))
|
||||||
|
.map(|(i, _)| -(i as isize) - 1)
|
||||||
|
} else {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.skip(cursor + 1)
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, f)| f.name().map_or(false, |n| self.matches(n)))
|
||||||
|
.map(|(i, _)| i as isize + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn ring(&self, files: &Files, cursor: usize, prev: bool) -> Option<isize> {
|
||||||
|
if prev {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.take(cursor + 1)
|
||||||
|
.rev()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, f)| f.name().map_or(false, |n| self.matches(n)))
|
||||||
|
.map(|(i, _)| -(i as isize))
|
||||||
|
.or_else(|| {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.skip(cursor + 1)
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, f)| f.name().map_or(false, |n| self.matches(n)))
|
||||||
|
.map(|(i, _)| i as isize + 1)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.skip(cursor)
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, f)| f.name().map_or(false, |n| self.matches(n)))
|
||||||
|
.map(|(i, _)| i as isize)
|
||||||
|
.or_else(|| {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.take(cursor)
|
||||||
|
.rev()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, f)| f.name().map_or(false, |n| self.matches(n)))
|
||||||
|
.map(|(i, _)| -(i as isize) - 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn catchup(&mut self, files: &Files) -> bool {
|
||||||
|
if self.version == files.version() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.matched.clear();
|
||||||
|
|
||||||
|
let mut i = 0u8;
|
||||||
|
for file in files.iter() {
|
||||||
|
if file.name().map(|n| self.matches(n)) != Some(true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.matched.insert(file.url_owned(), i);
|
||||||
|
if self.matched.len() > 99 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.version = files.version();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn matches(&self, name: &OsStr) -> bool {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
self.query.is_match(name.to_string_lossy().as_bytes())
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
self.query.is_match(name.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Finder {
|
||||||
|
#[inline]
|
||||||
|
pub fn matched(&self) -> &BTreeMap<Url, u8> { &self.matched }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn has_matched(&self) -> bool { !self.matched.is_empty() }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn matched_idx(&self, url: &Url) -> Option<u8> {
|
||||||
|
if let Some((_, &idx)) = self.matched.iter().find(|(u, _)| *u == url) {
|
||||||
|
return Some(idx);
|
||||||
|
}
|
||||||
|
if url.file_name().map(|n| self.matches(n)) == Some(true) {
|
||||||
|
return Some(100);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,7 +28,7 @@ impl Folder {
|
||||||
pub fn update(&mut self, op: FilesOp) -> bool {
|
pub fn update(&mut self, op: FilesOp) -> bool {
|
||||||
let b = match op {
|
let b = match op {
|
||||||
FilesOp::Full(_, items) => self.files.update_full(items),
|
FilesOp::Full(_, items) => self.files.update_full(items),
|
||||||
FilesOp::Part(_, version, items) => self.files.update_part(version, items),
|
FilesOp::Part(_, ticket, items) => self.files.update_part(ticket, items),
|
||||||
FilesOp::Size(_, items) => self.files.update_size(items),
|
FilesOp::Size(_, items) => self.files.update_size(items),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod finder;
|
||||||
mod folder;
|
mod folder;
|
||||||
mod manager;
|
mod manager;
|
||||||
mod mode;
|
mod mode;
|
||||||
|
|
@ -6,6 +7,7 @@ mod tab;
|
||||||
mod tabs;
|
mod tabs;
|
||||||
mod watcher;
|
mod watcher;
|
||||||
|
|
||||||
|
pub use finder::*;
|
||||||
pub use folder::*;
|
pub use folder::*;
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
pub use mode::*;
|
pub use mode::*;
|
||||||
|
|
|
||||||
|
|
@ -97,9 +97,9 @@ impl Preview {
|
||||||
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(500));
|
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(500));
|
||||||
pin!(rx);
|
pin!(rx);
|
||||||
|
|
||||||
let version = FilesOp::prepare(&url);
|
let ticket = FilesOp::prepare(&url);
|
||||||
while let Some(chunk) = rx.next().await {
|
while let Some(chunk) = rx.next().await {
|
||||||
emit!(Files(FilesOp::Part(url.clone(), version, chunk)));
|
emit!(Files(FilesOp::Part(url.clone(), ticket, chunk)));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ use shared::{MimeKind, PeekError};
|
||||||
use syntect::{easy::HighlightFile, util::as_24_bit_terminal_escaped};
|
use syntect::{easy::HighlightFile, util::as_24_bit_terminal_escaped};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
|
|
||||||
use super::PreviewData;
|
use super::PreviewData;
|
||||||
use crate::{external, highlighter};
|
use crate::{external, highlighter};
|
||||||
|
|
||||||
|
|
@ -85,7 +84,7 @@ impl Provider {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn highlight(path: &Path, skip: usize) -> Result<String, PeekError> {
|
pub(super) async fn highlight(path: &Path, skip: usize) -> Result<String, PeekError> {
|
||||||
let tick = INCR.load(Ordering::Relaxed);
|
let ticket = INCR.load(Ordering::Relaxed);
|
||||||
let path = path.to_path_buf();
|
let path = path.to_path_buf();
|
||||||
let spaces = " ".repeat(PREVIEW.tab_size as usize);
|
let spaces = " ".repeat(PREVIEW.tab_size as usize);
|
||||||
|
|
||||||
|
|
@ -98,7 +97,7 @@ impl Provider {
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
let limit = MANAGER.layout.preview_height();
|
let limit = MANAGER.layout.preview_height();
|
||||||
while h.reader.read_line(&mut line)? > 0 {
|
while h.reader.read_line(&mut line)? > 0 {
|
||||||
if tick != INCR.load(Ordering::Relaxed) {
|
if ticket != INCR.load(Ordering::Relaxed) {
|
||||||
return Err("Highlighting cancelled".into());
|
return Err("Highlighting cancelled".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
use std::{borrow::Cow, collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, mem, time::Duration};
|
use std::{borrow::Cow, collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, mem, time::Duration};
|
||||||
|
|
||||||
use anyhow::{bail, Error, Result};
|
use anyhow::{bail, Error, Result};
|
||||||
use config::open::Opener;
|
use config::{keymap::{Exec, KeymapLayer}, open::Opener};
|
||||||
use shared::{Defer, Url};
|
use shared::{Debounce, Defer, InputError, Url};
|
||||||
use tokio::{pin, task::JoinHandle};
|
use tokio::{pin, task::JoinHandle};
|
||||||
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||||
|
|
||||||
use super::{Folder, Mode, Preview, PreviewLock};
|
use super::{Finder, Folder, Mode, Preview, PreviewLock};
|
||||||
use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, files::{File, FilesOp, FilesSorter}, input::InputOpt, Event, BLOCKER};
|
use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, files::{File, FilesOp, FilesSorter}, input::InputOpt, Event, BLOCKER};
|
||||||
|
|
||||||
pub struct Tab {
|
pub struct Tab {
|
||||||
|
|
@ -17,6 +17,7 @@ pub struct Tab {
|
||||||
pub(super) history: BTreeMap<Url, Folder>,
|
pub(super) history: BTreeMap<Url, Folder>,
|
||||||
pub(super) preview: Preview,
|
pub(super) preview: Preview,
|
||||||
|
|
||||||
|
finder: Option<Finder>,
|
||||||
search: Option<JoinHandle<Result<()>>>,
|
search: Option<JoinHandle<Result<()>>>,
|
||||||
pub(super) show_hidden: bool,
|
pub(super) show_hidden: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -33,6 +34,7 @@ impl From<Url> for Tab {
|
||||||
history: Default::default(),
|
history: Default::default(),
|
||||||
preview: Default::default(),
|
preview: Default::default(),
|
||||||
|
|
||||||
|
finder: None,
|
||||||
search: None,
|
search: None,
|
||||||
show_hidden: true,
|
show_hidden: true,
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +47,11 @@ impl From<&Url> for Tab {
|
||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
pub fn escape(&mut self) -> bool {
|
pub fn escape(&mut self) -> bool {
|
||||||
|
if self.finder.is_some() {
|
||||||
|
self.finder = None;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((_, indices)) = self.mode.visual() {
|
if let Some((_, indices)) = self.mode.visual() {
|
||||||
self.current.files.select_index(indices, Some(self.mode.is_select()));
|
self.current.files.select_index(indices, Some(self.mode.is_select()));
|
||||||
self.mode = Mode::Normal;
|
self.mode = Mode::Normal;
|
||||||
|
|
@ -232,6 +239,49 @@ impl Tab {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find(&mut self, query: Option<&str>, prev: bool) -> bool {
|
||||||
|
if let Some(query) = query {
|
||||||
|
let Ok(finder) = Finder::new(query) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(step) = finder.ring(&self.current.files, self.current.cursor(), prev) {
|
||||||
|
self.arrow(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.finder = Some(finder);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let rx = emit!(Input(InputOpt::top("Find:").with_realtime()));
|
||||||
|
|
||||||
|
let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50));
|
||||||
|
pin!(rx);
|
||||||
|
|
||||||
|
while let Some(Ok(s)) | Some(Err(InputError::Typed(s))) = rx.next().await {
|
||||||
|
emit!(Call(
|
||||||
|
Exec::call("find", vec![s]).with_bool("previous", prev).vec(),
|
||||||
|
KeymapLayer::Manager
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_arrow(&mut self, prev: bool) -> bool {
|
||||||
|
let Some(finder) = &mut self.finder else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut b = finder.catchup(&self.current.files);
|
||||||
|
if let Some(step) = finder.arrow(&self.current.files, self.current.cursor(), prev) {
|
||||||
|
b |= self.arrow(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
pub fn search(&mut self, grep: bool) -> bool {
|
pub fn search(&mut self, grep: bool) -> bool {
|
||||||
if let Some(handle) = self.search.take() {
|
if let Some(handle) = self.search.take() {
|
||||||
handle.abort();
|
handle.abort();
|
||||||
|
|
@ -254,14 +304,14 @@ impl Tab {
|
||||||
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(300));
|
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(300));
|
||||||
pin!(rx);
|
pin!(rx);
|
||||||
|
|
||||||
let version = FilesOp::prepare(&cwd);
|
let ticket = FilesOp::prepare(&cwd);
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
while let Some(chunk) = rx.next().await {
|
while let Some(chunk) = rx.next().await {
|
||||||
if first {
|
if first {
|
||||||
emit!(Cd(cwd.clone()));
|
emit!(Cd(cwd.clone()));
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
emit!(Files(FilesOp::Part(cwd.clone(), version, chunk)));
|
emit!(Files(FilesOp::Part(cwd.clone(), ticket, chunk)));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
|
|
@ -409,6 +459,10 @@ impl Tab {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn preview_arrow(&mut self, step: isize) -> bool { self.preview.arrow(step) }
|
pub fn preview_arrow(&mut self, step: isize) -> bool { self.preview.arrow(step) }
|
||||||
|
|
||||||
|
// --- Finder
|
||||||
|
#[inline]
|
||||||
|
pub fn finder(&self) -> Option<&Finder> { self.finder.as_ref() }
|
||||||
|
|
||||||
// --- Sorter
|
// --- Sorter
|
||||||
pub fn set_sorter(&mut self, sorter: FilesSorter) -> bool {
|
pub fn set_sorter(&mut self, sorter: FilesSorter) -> bool {
|
||||||
if !self.current.files.set_sorter(sorter) {
|
if !self.current.files.set_sorter(sorter) {
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,10 @@ impl Which {
|
||||||
self.switch(false);
|
self.switch(false);
|
||||||
} else if self.cands.len() == 1 {
|
} else if self.cands.len() == 1 {
|
||||||
self.switch(false);
|
self.switch(false);
|
||||||
emit!(Ctrl(self.cands.remove(0), self.layer));
|
emit!(Call(self.cands[0].to_call(), self.layer));
|
||||||
} else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times + 1) {
|
} else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times + 1) {
|
||||||
self.switch(false);
|
self.switch(false);
|
||||||
emit!(Ctrl(self.cands.remove(i), self.layer));
|
emit!(Call(self.cands[i].to_call(), self.layer));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.times += 1;
|
self.times += 1;
|
||||||
|
|
|
||||||
62
shared/src/debounce.rs
Normal file
62
shared/src/debounce.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
use std::{pin::Pin, task::{Context, Poll}, time::Duration};
|
||||||
|
|
||||||
|
use futures::{FutureExt, Stream, StreamExt};
|
||||||
|
use tokio::time::{sleep, Instant, Sleep};
|
||||||
|
|
||||||
|
pub struct Debounce<S>
|
||||||
|
where
|
||||||
|
S: Stream,
|
||||||
|
{
|
||||||
|
stream: S,
|
||||||
|
interval: Duration,
|
||||||
|
|
||||||
|
sleep: Sleep,
|
||||||
|
last: Option<S::Item>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Debounce<S>
|
||||||
|
where
|
||||||
|
S: Stream + Unpin,
|
||||||
|
{
|
||||||
|
pub fn new(stream: S, interval: Duration) -> Debounce<S> {
|
||||||
|
Self { stream, interval, sleep: sleep(Duration::ZERO), last: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Stream for Debounce<S>
|
||||||
|
where
|
||||||
|
S: Stream + Unpin,
|
||||||
|
{
|
||||||
|
type Item = S::Item;
|
||||||
|
|
||||||
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
let (mut stream, interval, mut sleep, last) = unsafe {
|
||||||
|
let me = self.get_unchecked_mut();
|
||||||
|
(Pin::new(&mut me.stream), me.interval, Pin::new_unchecked(&mut me.sleep), &mut me.last)
|
||||||
|
};
|
||||||
|
|
||||||
|
if sleep.poll_unpin(cx).is_ready() {
|
||||||
|
if let Some(last) = last.take() {
|
||||||
|
return Poll::Ready(Some(last));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Poll::Ready(next) = stream.poll_next_unpin(cx) {
|
||||||
|
match next {
|
||||||
|
Some(next) => {
|
||||||
|
*last = Some(next);
|
||||||
|
}
|
||||||
|
None if last.is_none() => {
|
||||||
|
return Poll::Ready(None);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
sleep.reset(Instant::now());
|
||||||
|
return Poll::Pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep.reset(Instant::now() + interval);
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#![allow(clippy::option_map_unit_fn)]
|
#![allow(clippy::option_map_unit_fn)]
|
||||||
|
|
||||||
mod chars;
|
mod chars;
|
||||||
|
mod debounce;
|
||||||
mod defer;
|
mod defer;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod fns;
|
mod fns;
|
||||||
|
|
@ -13,6 +14,7 @@ mod time;
|
||||||
mod url;
|
mod url;
|
||||||
|
|
||||||
pub use chars::*;
|
pub use chars::*;
|
||||||
|
pub use debounce::*;
|
||||||
pub use defer::*;
|
pub use defer::*;
|
||||||
pub use errors::*;
|
pub use errors::*;
|
||||||
pub use fns::*;
|
pub use fns::*;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue