Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Navigate by typing file/directory names #659

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 58 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use cosmic::{
window::{self, Event as WindowEvent, Id as WindowId},
Alignment, Event, Length, Size, Subscription,
},
iced_core::SmolStr,
iced_runtime::clipboard,
style, theme,
widget::{
Expand Down Expand Up @@ -278,7 +279,7 @@ pub enum Message {
DialogUpdate(DialogPage),
DialogUpdateComplete(DialogPage),
ExtractHere(Option<Entity>),
Key(Modifiers, Key),
Key(Modifiers, Key, Option<SmolStr>),
LaunchUrl(String),
MaybeExit,
Modifiers(Modifiers),
Expand Down Expand Up @@ -471,6 +472,49 @@ pub struct FavoriteIndex(usize);

pub struct MounterData(MounterKey, MounterItem);

struct PrefixSearch {
search: String,
reset_delay: u64,
next_reset: Option<Instant>,
}

impl PrefixSearch {
pub fn new(reset_delay: u64) -> PrefixSearch {
PrefixSearch {
search: String::new(),
reset_delay,
next_reset: None,
}
}

pub fn reset(&mut self) {
self.search.clear();
self.next_reset = None;
}

pub fn update_search(&mut self, string: &str) {
// Clear the word when the last typed character is older then the reset delay
if let Some(next_reset) = self.next_reset {
if next_reset <= Instant::now() {
self.reset();
}
}
// Add the typed character
self.search.push_str(string);
// Restart the reset timeout
let delay = time::Duration::from_millis(self.reset_delay);
self.next_reset = Instant::now().checked_add(delay);
// Reset the search term when calculating the next reset failed
if self.next_reset.is_none() {
self.search.clear();
}
}

pub fn word(&self) -> &str {
&self.search
}
}

#[derive(Clone, Debug)]
pub enum WindowKind {
Desktop(Entity),
Expand Down Expand Up @@ -538,6 +582,7 @@ pub struct App {
tab_dnd_hover: Option<(Entity, Instant)>,
nav_drag_id: DragId,
tab_drag_id: DragId,
prefix_search: PrefixSearch,
}

impl App {
Expand Down Expand Up @@ -1466,6 +1511,7 @@ impl Application for App {
tab_dnd_hover: None,
nav_drag_id: DragId::new(),
tab_drag_id: DragId::new(),
prefix_search: PrefixSearch::new(500), // TODO do not hardcode delay?
};

let mut commands = vec![app.update_config()];
Expand Down Expand Up @@ -1938,13 +1984,21 @@ impl Application for App {
}
}
}
Message::Key(modifiers, key) => {
Message::Key(modifiers, key, text) => {
let entity = self.tab_model.active();
for (key_bind, action) in self.key_binds.iter() {
if key_bind.matches(modifiers, &key) {
self.prefix_search.reset();
return self.update(action.message(Some(entity)));
}
}
if let Some(text) = text {
self.prefix_search.update_search(&text);
return self.update(Message::TabMessage(
Some(entity),
tab::Message::SelectNextPrefix(self.prefix_search.word().into()),
));
}
}
Message::MaybeExit => {
if self.window_id_opt.is_none() && self.pending_operations.is_empty() {
Expand Down Expand Up @@ -4161,8 +4215,8 @@ impl Application for App {

let mut subscriptions = vec![
event::listen_with(|event, status, _window_id| match event {
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => match status {
event::Status::Ignored => Some(Message::Key(modifiers, key)),
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, text, ..}) => match status {
event::Status::Ignored => Some(Message::Key(modifiers, key, text)),
event::Status::Captured => None,
},
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
Expand Down
76 changes: 75 additions & 1 deletion src/tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use cosmic::{
Size,
Subscription,
},
iced_core::{mouse::ScrollDelta, widget::tree},
iced_core::{mouse::ScrollDelta, widget::tree, SmolStr},
theme,
widget::{
self,
Expand Down Expand Up @@ -1071,6 +1071,7 @@ pub enum Message {
SearchContext(Location, SearchContextWrapper),
SearchReady(bool),
SelectAll,
SelectNextPrefix(SmolStr),
SetSort(HeadingOptions, bool),
Thumbnail(PathBuf, ItemThumbnail),
ToggleShowHidden,
Expand Down Expand Up @@ -1857,6 +1858,68 @@ impl Tab {
}
}

pub fn select_next_prefix(&mut self, prefix: &str) {
*self.cached_selected.borrow_mut() = None;
if let Some(ref mut items) = self.items_opt {
// Special case: when all entered characters are the same, only search for said character.
// This allows quickly cycling through items starting with the same character.
let term = match prefix.chars().next() {
Some(first) if prefix.chars().all(|c| c == first) => &prefix[..1],
Some(_) => prefix,
None => return (),
};

// Have to add 1 to the start index when not reversing because the
// currently selected item should be included in until so it gets
// considered last instead of first.
// When ordered reverse, we don't want to do so because moving it
// last would put it as first item when iterating in reverse.
let start = self
.select_focus
.map_or(0, |i| if self.sort_direction { i + 1 } else { i });
let (until, after) = items.split_at_mut(start);

// First iterate over all items after the current selection, then wrap around
let iter = after
.iter_mut()
.enumerate()
.map(|x| (x.0 + start, x.1))
.chain(until.iter_mut().enumerate());

let found = if self.sort_direction {
Self::select_first_prefix(term, iter, self.config.show_hidden)
} else {
Self::select_first_prefix(term, iter.rev(), self.config.show_hidden)
};

if found.is_some() {
self.select_focus = found;
} else if let Some(focus) = self.select_focus {
items[focus].selected = true;
};
}
}

fn select_first_prefix<'a>(
prefix: &str,
iterator: impl Iterator<Item = (usize, &'a mut Item)>,
consider_hidden: bool,
) -> Option<usize> {
let mut selected = None;
for (i, item) in iterator {
if selected.is_none()
&& (!item.hidden || consider_hidden)
&& item.display_name.to_lowercase().starts_with(prefix)
{
selected = Some(i);
item.selected = true;
} else {
item.selected = false;
}
}
selected
}

pub fn select_path(&mut self, path: PathBuf) {
let location = Location::Path(path);
*self.cached_selected.borrow_mut() = None;
Expand Down Expand Up @@ -2807,6 +2870,17 @@ impl Tab {
));
}
}
Message::SelectNextPrefix(s) => {
self.select_next_prefix(&s);
if let Some(offset) = self.select_focus_scroll() {
commands.push(Command::Iced(
scrollable::scroll_to(self.scrollable_id.clone(), offset).into(),
));
}
if let Some(id) = self.select_focus_id() {
commands.push(Command::Iced(widget::button::focus(id).into()));
}
}
Message::SetSort(heading_option, dir) => {
if !matches!(self.location, Location::Search(..)) {
self.sort_name = heading_option;
Expand Down