From f2b4f01242660a71985c5f68e3177d53a47ce03e Mon Sep 17 00:00:00 2001 From: Moshe Sherman Date: Mon, 5 Oct 2020 14:50:12 +0300 Subject: [PATCH] Vim like search (#279) * add quick search within a list * vim like search navigation * close cmd line with esc * format * document changes in README --- README.md | 9 +++++-- src/command.rs | 11 ++++++++ src/commands.rs | 10 +++++--- src/main.rs | 33 +++++++++++++++++++++--- src/ui/layout.rs | 7 ++++++ src/ui/listview.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 121 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 75a5685..004ac06 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/command.rs b/src/command.rs index 63d0076..301be7f 100644 --- a/src/command.rs +++ b/src/command.rs @@ -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), 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 { _ => None, }) .map(Command::Open), + "jump" => Some(Command::Jump(JumpMode::Query(args.join(" ")))), "search" => args .get(0) .map(|query| Command::Search((*query).to_string())), diff --git a/src/commands.rs b/src/commands.rs index 736784c..30bbbd7 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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( diff --git a/src/main.rs b/src/main.rs index 965d445..2eb0cfd 100644 --- a/src/main.rs +++ b/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").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").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::("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::().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::().cloned() { + data.cmd.handle(s, parsed) + } } } ev.trigger(); diff --git a/src/ui/layout.rs b/src/ui/layout.rs index 5cfb558..b4ffbff 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -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, T: IntoBoxedViewExt>(&mut self, id: S, view: T, title: S) { let s = id.into(); let screen = Screen { diff --git a/src/ui/listview.rs b/src/ui/listview.rs index 2280043..8a0e4a2 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -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 { content: Arc>>, last_content_len: usize, selected: usize, + search_indexes: Vec, + search_selected_index: usize, last_size: Vec2, scrollbar: ScrollBase, queue: Arc, @@ -102,6 +104,8 @@ impl ListView { 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 ListView { self.selected } + pub fn get_indexes_of(&self, query: &String) -> Vec { + 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 ViewExt for ListView { 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);