Vim like search (#279)

* add quick search within a list

* vim like search navigation

* close cmd line with esc

* format

* document changes in README
This commit is contained in:
Moshe Sherman
2020-10-05 14:50:12 +03:00
committed by GitHub
parent fc79889665
commit f2b4f01242
6 changed files with 121 additions and 12 deletions

View File

@@ -80,8 +80,8 @@ depending on your desktop environment settings. Have a look at the
* `F3`: Library
* `d` deletes the currently selected playlist
* Tracks and playlists can be played using `Return` and queued using `Space`
* `n` will play the selected item after the currently playing track
* `.` will move to the currently playing track in the queue
* `.` will play the selected item after the currently playing track
* `p` will move to the currently playing track in the queue
* `s` will save, `d` will remove the currently selected track to/from your
library
* `o` will open a detail view or context menu for the selected item
@@ -102,6 +102,9 @@ depending on your desktop environment settings. Have a look at the
* `x` copies a sharable URL of the song to the system clipboard
* `Shift-x` copies a sharable URL of the currently selected item to the system clipboard
Use `/` to open a Vim-like search bar, you can use `n` and `N` to go for the next/previous
search occurrence, respectivly.
You can also open a Vim style commandprompt using `:`, the following commands
are supported:
@@ -116,6 +119,8 @@ The screens can be opened with `queue`, `search`, `playlists` and `log`, whereas
`search` can be supplied with a search term that will be entered after opening
the search view.
To close the commandprompt at any time, press `esc`.
## Configuration
Configuration is saved to `~/.config/ncspot/config.toml`. To reload the

View File

@@ -42,6 +42,14 @@ impl Default for MoveAmount {
}
}
#[derive(Display, Clone, Serialize, Deserialize, Debug)]
#[strum(serialize_all = "lowercase")]
pub enum JumpMode {
Previous,
Next,
Query(String),
}
#[derive(Display, Clone, Serialize, Deserialize, Debug)]
#[strum(serialize_all = "lowercase")]
pub enum ShiftMode {
@@ -102,6 +110,7 @@ pub enum Command {
Move(MoveMode, MoveAmount),
Shift(ShiftMode, Option<i32>),
Search(String),
Jump(JumpMode),
Help,
ReloadConfig,
Noop,
@@ -157,6 +166,7 @@ impl fmt::Display for Command {
Command::Move(mode, MoveAmount::Integer(amount)) => format!("move {} {}", mode, amount),
Command::Shift(mode, amount) => format!("shift {} {}", mode, amount.unwrap_or(1)),
Command::Search(term) => format!("search {}", term),
Command::Jump(term) => format!("jump {}", term),
Command::Help => "help".to_string(),
Command::ReloadConfig => "reload".to_string(),
};
@@ -224,6 +234,7 @@ pub fn parse(input: &str) -> Option<Command> {
_ => None,
})
.map(Command::Open),
"jump" => Some(Command::Jump(JumpMode::Query(args.join(" ")))),
"search" => args
.get(0)
.map(|query| Command::Search((*query).to_string())),

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use std::time::Duration;
use crate::command::{
parse, Command, GotoMode, MoveAmount, MoveMode, SeekDirection, ShiftMode, TargetMode,
parse, Command, GotoMode, JumpMode, MoveAmount, MoveMode, SeekDirection, ShiftMode, TargetMode,
};
use crate::config::Config;
use crate::library::Library;
@@ -174,6 +174,7 @@ impl CommandManager {
Ok(None)
}
Command::Search(_)
| Command::Jump(_)
| Command::Move(_, _)
| Command::Shift(_, _)
| Command::Play
@@ -270,12 +271,13 @@ impl CommandManager {
kb.insert(">".into(), Command::Next);
kb.insert("c".into(), Command::Clear);
kb.insert("Space".into(), Command::Queue);
kb.insert("n".into(), Command::PlayNext);
kb.insert(".".into(), Command::PlayNext);
kb.insert("Enter".into(), Command::Play);
kb.insert("n".into(), Command::Jump(JumpMode::Next));
kb.insert("Shift+n".into(), Command::Jump(JumpMode::Previous));
kb.insert("s".into(), Command::Save);
kb.insert("Ctrl+s".into(), Command::SaveQueue);
kb.insert("d".into(), Command::Delete);
kb.insert("/".into(), Command::Focus("search".into()));
kb.insert("f".into(), Command::Seek(SeekDirection::Relative(1000)));
kb.insert("b".into(), Command::Seek(SeekDirection::Relative(-1000)));
kb.insert(
@@ -306,7 +308,7 @@ impl CommandManager {
kb.insert("Up".into(), Command::Move(MoveMode::Up, Default::default()));
kb.insert(
".".into(),
"p".into(),
Command::Move(MoveMode::Playing, Default::default()),
);
kb.insert(

View File

@@ -74,6 +74,7 @@ mod ui;
#[cfg(feature = "mpris")]
mod mpris;
use crate::command::{Command, JumpMode};
use crate::commands::CommandManager;
use crate::events::{Event, EventManager};
use crate::library::Library;
@@ -279,6 +280,22 @@ fn main() {
}
});
cursive.add_global_callback('/', move |s| {
if s.find_name::<ContextMenu>("contextmenu").is_none() {
s.call_on_name("main", |v: &mut ui::layout::Layout| {
v.enable_jump();
});
}
});
cursive.add_global_callback(cursive::event::Key::Esc, move |s| {
if s.find_name::<ContextMenu>("contextmenu").is_none() {
s.call_on_name("main", |v: &mut ui::layout::Layout| {
v.clear_cmdline();
});
}
});
layout.cmdline.set_on_edit(move |s, cmd, _| {
s.call_on_name("main", |v: &mut ui::layout::Layout| {
if cmd.is_empty() {
@@ -294,11 +311,19 @@ fn main() {
let mut main = s.find_name::<ui::layout::Layout>("main").unwrap();
main.clear_cmdline();
}
let c = &cmd[1..];
let parsed = command::parse(c);
if let Some(parsed) = parsed {
if cmd.starts_with("/") {
let query = &cmd[1..];
let command = Command::Jump(JumpMode::Query(query.to_string()));
if let Some(data) = s.user_data::<UserData>().cloned() {
data.cmd.handle(s, parsed)
data.cmd.handle(s, command);
}
} else {
let c = &cmd[1..];
let parsed = command::parse(c);
if let Some(parsed) = parsed {
if let Some(data) = s.user_data::<UserData>().cloned() {
data.cmd.handle(s, parsed)
}
}
}
ev.trigger();

View File

@@ -67,6 +67,13 @@ impl Layout {
}
}
pub fn enable_jump(&mut self) {
if !self.cmdline_focus {
self.cmdline.set_content("/");
self.cmdline_focus = true;
}
}
pub fn add_view<S: Into<String>, T: IntoBoxedViewExt>(&mut self, id: S, view: T, title: S) {
let s = id.into();
let screen = Screen {

View File

@@ -1,4 +1,4 @@
use std::cmp::{max, min};
use std::cmp::{max, min, Ordering};
use std::sync::{Arc, RwLock};
use cursive::align::HAlign;
@@ -9,7 +9,7 @@ use cursive::view::ScrollBase;
use cursive::{Cursive, Printer, Rect, Vec2};
use unicode_width::UnicodeWidthStr;
use crate::command::{Command, GotoMode, MoveAmount, MoveMode, TargetMode};
use crate::command::{Command, GotoMode, JumpMode, MoveAmount, MoveMode, TargetMode};
use crate::commands::CommandResult;
use crate::library::Library;
use crate::playable::Playable;
@@ -89,6 +89,8 @@ pub struct ListView<I: ListItem> {
content: Arc<RwLock<Vec<I>>>,
last_content_len: usize,
selected: usize,
search_indexes: Vec<usize>,
search_selected_index: usize,
last_size: Vec2,
scrollbar: ScrollBase,
queue: Arc<Queue>,
@@ -102,6 +104,8 @@ impl<I: ListItem> ListView<I> {
content,
last_content_len: 0,
selected: 0,
search_indexes: Vec::new(),
search_selected_index: 0,
last_size: Vec2::new(0, 0),
scrollbar: ScrollBase::new(),
queue,
@@ -132,6 +136,20 @@ impl<I: ListItem> ListView<I> {
self.selected
}
pub fn get_indexes_of(&self, query: &String) -> Vec<usize> {
let content = self.content.read().unwrap();
content
.iter()
.enumerate()
.filter(|(_, i)| {
i.display_left()
.to_lowercase()
.contains(&query[..].to_lowercase())
})
.map(|(i, _)| i)
.collect()
}
pub fn move_focus_to(&mut self, target: usize) {
let len = self.content.read().unwrap().len().saturating_sub(1);
self.selected = min(target, len);
@@ -395,6 +413,47 @@ impl<I: ListItem + Clone> ViewExt for ListView<I> {
return Ok(CommandResult::Consumed(None));
}
Command::Jump(mode) => match mode {
JumpMode::Query(query) => {
self.search_indexes = self.get_indexes_of(query);
self.search_selected_index = 0;
match self.search_indexes.get(0) {
Some(&index) => {
self.move_focus_to(index);
return Ok(CommandResult::Consumed(None));
}
None => return Ok(CommandResult::Ignored),
}
}
JumpMode::Next => {
let len = self.search_indexes.len();
if len == 0 {
return Ok(CommandResult::Ignored);
}
let index = self.search_selected_index;
let next_index = match index.cmp(&(len - 1)) {
Ordering::Equal => 0,
_ => index + 1,
};
self.move_focus_to(self.search_indexes[next_index]);
self.search_selected_index = next_index;
return Ok(CommandResult::Consumed(None));
}
JumpMode::Previous => {
let len = self.search_indexes.len();
if len == 0 {
return Ok(CommandResult::Ignored);
}
let index = self.search_selected_index;
let prev_index = match index.cmp(&0) {
Ordering::Equal => len - 1,
_ => index - 1,
};
self.move_focus_to(self.search_indexes[prev_index]);
self.search_selected_index = prev_index;
return Ok(CommandResult::Consumed(None));
}
},
Command::Move(mode, amount) => {
let last_idx = self.content.read().unwrap().len().saturating_sub(1);