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
|
* `F3`: Library
|
||||||
* `d` deletes the currently selected playlist
|
* `d` deletes the currently selected playlist
|
||||||
* Tracks and playlists can be played using `Return` and queued using `Space`
|
* Tracks and playlists can be played using `Return` and queued using `Space`
|
||||||
* `n` will play the selected item after the currently playing track
|
* `.` will play the selected item after the currently playing track
|
||||||
* `.` will move to the currently playing track in the queue
|
* `p` will move to the currently playing track in the queue
|
||||||
* `s` will save, `d` will remove the currently selected track to/from your
|
* `s` will save, `d` will remove the currently selected track to/from your
|
||||||
library
|
library
|
||||||
* `o` will open a detail view or context menu for the selected item
|
* `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
|
* `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
|
* `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
|
You can also open a Vim style commandprompt using `:`, the following commands
|
||||||
are supported:
|
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
|
`search` can be supplied with a search term that will be entered after opening
|
||||||
the search view.
|
the search view.
|
||||||
|
|
||||||
|
To close the commandprompt at any time, press `esc`.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration is saved to `~/.config/ncspot/config.toml`. To reload the
|
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)]
|
#[derive(Display, Clone, Serialize, Deserialize, Debug)]
|
||||||
#[strum(serialize_all = "lowercase")]
|
#[strum(serialize_all = "lowercase")]
|
||||||
pub enum ShiftMode {
|
pub enum ShiftMode {
|
||||||
@@ -102,6 +110,7 @@ pub enum Command {
|
|||||||
Move(MoveMode, MoveAmount),
|
Move(MoveMode, MoveAmount),
|
||||||
Shift(ShiftMode, Option<i32>),
|
Shift(ShiftMode, Option<i32>),
|
||||||
Search(String),
|
Search(String),
|
||||||
|
Jump(JumpMode),
|
||||||
Help,
|
Help,
|
||||||
ReloadConfig,
|
ReloadConfig,
|
||||||
Noop,
|
Noop,
|
||||||
@@ -157,6 +166,7 @@ impl fmt::Display for Command {
|
|||||||
Command::Move(mode, MoveAmount::Integer(amount)) => format!("move {} {}", mode, amount),
|
Command::Move(mode, MoveAmount::Integer(amount)) => format!("move {} {}", mode, amount),
|
||||||
Command::Shift(mode, amount) => format!("shift {} {}", mode, amount.unwrap_or(1)),
|
Command::Shift(mode, amount) => format!("shift {} {}", mode, amount.unwrap_or(1)),
|
||||||
Command::Search(term) => format!("search {}", term),
|
Command::Search(term) => format!("search {}", term),
|
||||||
|
Command::Jump(term) => format!("jump {}", term),
|
||||||
Command::Help => "help".to_string(),
|
Command::Help => "help".to_string(),
|
||||||
Command::ReloadConfig => "reload".to_string(),
|
Command::ReloadConfig => "reload".to_string(),
|
||||||
};
|
};
|
||||||
@@ -224,6 +234,7 @@ pub fn parse(input: &str) -> Option<Command> {
|
|||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.map(Command::Open),
|
.map(Command::Open),
|
||||||
|
"jump" => Some(Command::Jump(JumpMode::Query(args.join(" ")))),
|
||||||
"search" => args
|
"search" => args
|
||||||
.get(0)
|
.get(0)
|
||||||
.map(|query| Command::Search((*query).to_string())),
|
.map(|query| Command::Search((*query).to_string())),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::command::{
|
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::config::Config;
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
@@ -174,6 +174,7 @@ impl CommandManager {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Command::Search(_)
|
Command::Search(_)
|
||||||
|
| Command::Jump(_)
|
||||||
| Command::Move(_, _)
|
| Command::Move(_, _)
|
||||||
| Command::Shift(_, _)
|
| Command::Shift(_, _)
|
||||||
| Command::Play
|
| Command::Play
|
||||||
@@ -270,12 +271,13 @@ impl CommandManager {
|
|||||||
kb.insert(">".into(), Command::Next);
|
kb.insert(">".into(), Command::Next);
|
||||||
kb.insert("c".into(), Command::Clear);
|
kb.insert("c".into(), Command::Clear);
|
||||||
kb.insert("Space".into(), Command::Queue);
|
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("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("s".into(), Command::Save);
|
||||||
kb.insert("Ctrl+s".into(), Command::SaveQueue);
|
kb.insert("Ctrl+s".into(), Command::SaveQueue);
|
||||||
kb.insert("d".into(), Command::Delete);
|
kb.insert("d".into(), Command::Delete);
|
||||||
kb.insert("/".into(), Command::Focus("search".into()));
|
|
||||||
kb.insert("f".into(), Command::Seek(SeekDirection::Relative(1000)));
|
kb.insert("f".into(), Command::Seek(SeekDirection::Relative(1000)));
|
||||||
kb.insert("b".into(), Command::Seek(SeekDirection::Relative(-1000)));
|
kb.insert("b".into(), Command::Seek(SeekDirection::Relative(-1000)));
|
||||||
kb.insert(
|
kb.insert(
|
||||||
@@ -306,7 +308,7 @@ impl CommandManager {
|
|||||||
|
|
||||||
kb.insert("Up".into(), Command::Move(MoveMode::Up, Default::default()));
|
kb.insert("Up".into(), Command::Move(MoveMode::Up, Default::default()));
|
||||||
kb.insert(
|
kb.insert(
|
||||||
".".into(),
|
"p".into(),
|
||||||
Command::Move(MoveMode::Playing, Default::default()),
|
Command::Move(MoveMode::Playing, Default::default()),
|
||||||
);
|
);
|
||||||
kb.insert(
|
kb.insert(
|
||||||
|
|||||||
33
src/main.rs
33
src/main.rs
@@ -74,6 +74,7 @@ mod ui;
|
|||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
mod mpris;
|
mod mpris;
|
||||||
|
|
||||||
|
use crate::command::{Command, JumpMode};
|
||||||
use crate::commands::CommandManager;
|
use crate::commands::CommandManager;
|
||||||
use crate::events::{Event, EventManager};
|
use crate::events::{Event, EventManager};
|
||||||
use crate::library::Library;
|
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, _| {
|
layout.cmdline.set_on_edit(move |s, cmd, _| {
|
||||||
s.call_on_name("main", |v: &mut ui::layout::Layout| {
|
s.call_on_name("main", |v: &mut ui::layout::Layout| {
|
||||||
if cmd.is_empty() {
|
if cmd.is_empty() {
|
||||||
@@ -294,11 +311,19 @@ fn main() {
|
|||||||
let mut main = s.find_name::<ui::layout::Layout>("main").unwrap();
|
let mut main = s.find_name::<ui::layout::Layout>("main").unwrap();
|
||||||
main.clear_cmdline();
|
main.clear_cmdline();
|
||||||
}
|
}
|
||||||
let c = &cmd[1..];
|
if cmd.starts_with("/") {
|
||||||
let parsed = command::parse(c);
|
let query = &cmd[1..];
|
||||||
if let Some(parsed) = parsed {
|
let command = Command::Jump(JumpMode::Query(query.to_string()));
|
||||||
if let Some(data) = s.user_data::<UserData>().cloned() {
|
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();
|
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) {
|
pub fn add_view<S: Into<String>, T: IntoBoxedViewExt>(&mut self, id: S, view: T, title: S) {
|
||||||
let s = id.into();
|
let s = id.into();
|
||||||
let screen = Screen {
|
let screen = Screen {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::cmp::{max, min};
|
use std::cmp::{max, min, Ordering};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use cursive::align::HAlign;
|
use cursive::align::HAlign;
|
||||||
@@ -9,7 +9,7 @@ use cursive::view::ScrollBase;
|
|||||||
use cursive::{Cursive, Printer, Rect, Vec2};
|
use cursive::{Cursive, Printer, Rect, Vec2};
|
||||||
use unicode_width::UnicodeWidthStr;
|
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::commands::CommandResult;
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
use crate::playable::Playable;
|
use crate::playable::Playable;
|
||||||
@@ -89,6 +89,8 @@ pub struct ListView<I: ListItem> {
|
|||||||
content: Arc<RwLock<Vec<I>>>,
|
content: Arc<RwLock<Vec<I>>>,
|
||||||
last_content_len: usize,
|
last_content_len: usize,
|
||||||
selected: usize,
|
selected: usize,
|
||||||
|
search_indexes: Vec<usize>,
|
||||||
|
search_selected_index: usize,
|
||||||
last_size: Vec2,
|
last_size: Vec2,
|
||||||
scrollbar: ScrollBase,
|
scrollbar: ScrollBase,
|
||||||
queue: Arc<Queue>,
|
queue: Arc<Queue>,
|
||||||
@@ -102,6 +104,8 @@ impl<I: ListItem> ListView<I> {
|
|||||||
content,
|
content,
|
||||||
last_content_len: 0,
|
last_content_len: 0,
|
||||||
selected: 0,
|
selected: 0,
|
||||||
|
search_indexes: Vec::new(),
|
||||||
|
search_selected_index: 0,
|
||||||
last_size: Vec2::new(0, 0),
|
last_size: Vec2::new(0, 0),
|
||||||
scrollbar: ScrollBase::new(),
|
scrollbar: ScrollBase::new(),
|
||||||
queue,
|
queue,
|
||||||
@@ -132,6 +136,20 @@ impl<I: ListItem> ListView<I> {
|
|||||||
self.selected
|
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) {
|
pub fn move_focus_to(&mut self, target: usize) {
|
||||||
let len = self.content.read().unwrap().len().saturating_sub(1);
|
let len = self.content.read().unwrap().len().saturating_sub(1);
|
||||||
self.selected = min(target, len);
|
self.selected = min(target, len);
|
||||||
@@ -395,6 +413,47 @@ impl<I: ListItem + Clone> ViewExt for ListView<I> {
|
|||||||
|
|
||||||
return Ok(CommandResult::Consumed(None));
|
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) => {
|
Command::Move(mode, amount) => {
|
||||||
let last_idx = self.content.read().unwrap().len().saturating_sub(1);
|
let last_idx = self.content.read().unwrap().len().saturating_sub(1);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user