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:
@@ -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
|
||||
|
||||
@@ -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())),
|
||||
|
||||
@@ -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(
|
||||
|
||||
33
src/main.rs
33
src/main.rs
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user