diff --git a/src/commands.rs b/src/commands.rs index c4528f7..76819dc 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,11 +1,22 @@ use std::collections::HashMap; +use std::sync::Arc; use cursive::Cursive; +use cursive::event::{Event, Key}; + +use queue::Queue; +use spotify::Spotify; +use playlists::Playlist; +use track::Track; +use ui::layout::Layout; +use ui::listview::ListView; +use ui::search::SearchView; pub struct CommandManager { commands: HashMap) -> Result, String>>>, aliases: HashMap, + callbacks: Vec ()>> } impl CommandManager { @@ -13,6 +24,7 @@ impl CommandManager { CommandManager { commands: HashMap::new(), aliases: HashMap::new(), + callbacks: Vec::new(), } } @@ -29,6 +41,265 @@ impl CommandManager { self.commands.insert(name, cb); } + pub fn register_all(&mut self, spotify: Arc, queue: Arc) { + self.register( + "quit", + vec!["q", "x"], + Box::new(move |s, _args| { + s.quit(); + Ok(None) + }), + ); + + { + let queue = queue.clone(); + self.register( + "stop", + Vec::new(), + Box::new(move |_s, _args| { + queue.stop(); + Ok(None) + }), + ); + } + + { + let queue = queue.clone(); + self.register( + "previous", + Vec::new(), + Box::new(move |_s, _args| { + queue.previous(); + Ok(None) + }), + ); + } + + { + let queue = queue.clone(); + self.register( + "next", + Vec::new(), + Box::new(move |_s, _args| { + queue.next(); + Ok(None) + }), + ); + } + + { + let queue = queue.clone(); + self.register( + "clear", + Vec::new(), + Box::new(move |_s, _args| { + queue.clear(); + Ok(None) + }), + ); + } + + { + let spotify = spotify.clone(); + self.register( + "search", + Vec::new(), + Box::new(move |s, args| { + s.call_on_id("main", |v: &mut Layout| { + v.set_view("search"); + }); + s.call_on_id("search", |v: &mut SearchView| { + if args.len() >= 1 { + v.run_search(args.join(" "), spotify.clone()); + } else { + v.focus_search(); + } + }); + Ok(None) + }), + ); + } + + { + self.register( + "playlists", + vec!["lists"], + Box::new(move |s, _args| { + s.call_on_id("main", |v: &mut Layout| { + v.set_view("playlists"); + }); + Ok(None) + }), + ); + } + + self.register( + "log", + Vec::new(), + Box::new(move |s, _args| { + s.call_on_id("main", |v: &mut Layout| { + v.set_view("log"); + }); + Ok(None) + }), + ); + + self.register( + "move", + Vec::new(), + Box::new(move |s, args| { + if args.len() < 1 { + return Err("Missing direction (up, down, left, right)".to_string()); + } + + let dir = args.get(0).unwrap(); + + let amount: i32 = args.get(1).unwrap_or(&"1".to_string()).parse().map_err(|e| format!("{:?}", e))?; + + if dir == "up" || dir == "down" { + let dir = if dir == "up" { -1 } else { 1 }; + s.call_on_id("queue_list", |v: &mut ListView| { + v.move_focus(dir * amount); + }); + s.call_on_id("list", |v: &mut ListView| { + v.move_focus(dir * amount); + }); + s.call_on_id("list", |v: &mut ListView| { + v.move_focus(dir * amount); + }); + s.on_event(Event::Refresh); + return Ok(None); + } + + if dir == "left" || dir == "right" { + return Ok(None); + } + + Err(format!("Unrecognized direction: {}", dir)) + }) + ); + + { + let queue = queue.clone(); + self.register( + "queue", + Vec::new(), + Box::new(move |s, args| { + if let Some(arg) = args.get(0) { + if arg != "selected" { + return Err("".into()); + } + } else { + s.call_on_id("main", |v: &mut Layout| { + v.set_view("queue"); + }); + return Ok(None); + } + + { + let queue = queue.clone(); + s.call_on_id("list", |v: &mut ListView| { + v.with_selected(Box::new(move |t| { + queue.append(t); + })); + }); + } + + { + let queue = queue.clone(); + s.call_on_id("list", |v: &mut ListView| { + v.with_selected(Box::new(move |pl| { + for track in pl.tracks.iter() { + queue.append(track); + } + })); + }); + } + + Ok(None) + }), + ); + } + + { + let queue = queue.clone(); + self.register( + "play", + vec!["pause", "toggle", "toggleplay", "toggleplayback"], + Box::new(move |s, args| { + if let Some(arg) = args.get(0) { + if arg != "selected" { + return Err("".into()); + } + } else { + queue.toggleplayback(); + return Ok(None); + } + + { + let queue = queue.clone(); + s.call_on_id("queue_list", |v: &mut ListView| { + v.get_selected_index().map(|i| queue.play(i)); + }); + } + + { + let queue = queue.clone(); + s.call_on_id("list", |v: &mut ListView| { + v.with_selected(Box::new(move |t| { + let index = queue.append_next(t); + queue.play(index); + })); + }); + } + + { + let queue = queue.clone(); + s.call_on_id("list", |v: &mut ListView| { + v.with_selected(Box::new(move |pl| { + let indices: Vec = pl.tracks + .iter() + .map(|t| queue.append_next(t)) + .collect(); + if let Some(i) = indices.get(0) { + queue.play(*i) + } + })); + }); + } + + Ok(None) + }), + ); + } + + { + let queue = queue.clone(); + self.register( + "delete", + Vec::new(), + Box::new(move |s, args| { + if let Some(arg) = args.get(0) { + if arg != "selected" { + return Err("".into()); + } + } else { + return Err("".into()); + } + + { + let queue = queue.clone(); + s.call_on_id("queue_list", |v: &mut ListView| { + v.get_selected_index().map(|i| queue.remove(i)); + }); + } + + Ok(None) + }), + ); + } + } + fn handle_aliases(&self, name: &String) -> String { if let Some(s) = self.aliases.get(name) { self.handle_aliases(s) @@ -37,13 +308,125 @@ impl CommandManager { } } - pub fn handle(&self, s: &mut Cursive, cmd: String) -> Result, String> { + pub fn handle(&self, s: &mut Cursive, cmd: String) { let components: Vec = cmd.split(' ').map(|s| s.to_string()).collect(); - if let Some(cb) = self.commands.get(&self.handle_aliases(&components[0])) { + let result = if let Some(cb) = self.commands.get(&self.handle_aliases(&components[0])) { cb(s, components[1..].to_vec()) } else { Err("Unknown command.".to_string()) + }; + + // TODO: handle non-error output as well + if let Err(e) = result { + s.call_on_id("main", |v: &mut Layout| { + v.set_error(e); + }); + } + + for cb in &self.callbacks { + cb(); + } + } + + pub fn register_callback(&mut self, cb: Box ()>) { + self.callbacks.push(cb); + } + + pub fn register_keybinding<'a, E: Into, S: Into>( + this: Arc, + cursive: &'a mut Cursive, + event: E, + command: S, + ) { + let cmd = command.into(); + cursive.add_global_callback(event, move |s| { + this.handle(s, cmd.clone()); + }); + } + + pub fn register_keybindings<'a>( + this: Arc, + cursive: &'a mut Cursive, + keybindings: Option> + ) { + let mut kb = Self::default_keybindings(); + kb.extend(keybindings.unwrap_or(HashMap::new())); + + for (k, v) in kb { + Self::register_keybinding( + this.clone(), + cursive, + Self::parse_keybinding(k), + v); + } + } + + fn default_keybindings() -> HashMap { + let mut kb = HashMap::new(); + + kb.insert("q".into(), "quit".into()); + kb.insert("P".into(), "toggle".into()); + kb.insert("S".into(), "stop".into()); + kb.insert("<".into(), "previous".into()); + kb.insert(">".into(), "next".into()); + kb.insert("c".into(), "clear".into()); + kb.insert(" ".into(), "queue selected".into()); + kb.insert("Enter".into(), "play selected".into()); + kb.insert("d".into(), "delete selected".into()); + kb.insert("/".into(), "search".into()); + + kb.insert("F1".into(), "queue".into()); + kb.insert("F2".into(), "search".into()); + kb.insert("F3".into(), "playlists".into()); + kb.insert("F9".into(), "log".into()); + + kb.insert("Up".into(), "move up".into()); + kb.insert("Down".into(), "move down".into()); + kb.insert("Left".into(), "move left".into()); + kb.insert("Right".into(), "move right".into()); + kb.insert("k".into(), "move up".into()); + kb.insert("j".into(), "move down".into()); + kb.insert("h".into(), "move left".into()); + kb.insert("l".into(), "move right".into()); + + kb + } + + fn parse_keybinding(kb: String) -> cursive::event::Event { + match kb.as_ref() { + "Enter" => Event::Key(Key::Enter), + "Tab" => Event::Key(Key::Tab), + "Backspace" => Event::Key(Key::Backspace), + "Esc" => Event::Key(Key::Esc), + "Left" => Event::Key(Key::Left), + "Right" => Event::Key(Key::Right), + "Up" => Event::Key(Key::Up), + "Down" => Event::Key(Key::Down), + "Ins" => Event::Key(Key::Ins), + "Del" => Event::Key(Key::Del), + "Home" => Event::Key(Key::Home), + "End" => Event::Key(Key::End), + "PageUp" => Event::Key(Key::PageUp), + "PageDown" => Event::Key(Key::PageDown), + "PauseBreak" => Event::Key(Key::PauseBreak), + "NumpadCenter" => Event::Key(Key::NumpadCenter), + "F0" => Event::Key(Key::F0), + "F1" => Event::Key(Key::F1), + "F2" => Event::Key(Key::F2), + "F3" => Event::Key(Key::F3), + "F4" => Event::Key(Key::F4), + "F5" => Event::Key(Key::F5), + "F6" => Event::Key(Key::F6), + "F7" => Event::Key(Key::F7), + "F8" => Event::Key(Key::F8), + "F9" => Event::Key(Key::F9), + "F10" => Event::Key(Key::F10), + "F11" => Event::Key(Key::F11), + "F12" => Event::Key(Key::F12), + s => { + Event::Char(s.chars().next().unwrap()) + } } } } diff --git a/src/config.rs b/src/config.rs index 2224eb6..7e07fd6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,10 @@ +use std::collections::HashMap; + pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b"; #[derive(Serialize, Deserialize, Debug, Default)] pub struct Config { pub username: String, pub password: String, + pub keybindings: Option>, } diff --git a/src/main.rs b/src/main.rs index aba4915..dd8f2a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ extern crate crossbeam_channel; +#[macro_use] extern crate cursive; extern crate failure; extern crate futures; @@ -28,12 +29,10 @@ use std::io::prelude::*; use std::path::PathBuf; use std::process; use std::sync::Arc; -use std::sync::Mutex; use std::thread; -use cursive::event::Key; -use cursive::traits::{Identifiable, View}; -use cursive::view::{ScrollStrategy, Selector}; +use cursive::traits::{Identifiable}; +use cursive::view::{ScrollStrategy}; use cursive::views::*; use cursive::Cursive; @@ -46,6 +45,7 @@ mod spotify; mod theme; mod track; mod ui; +mod traits; #[cfg(feature = "mpris")] mod mpris; @@ -81,19 +81,6 @@ fn init_logger(content: TextContent, write_to_file: bool) { } } -fn register_keybinding, S: Into>( - cursive: &mut Cursive, - ev: &EventManager, - event: E, - command: S, -) { - let ev = ev.clone(); - let cmd = command.into(); - cursive.add_global_callback(event, move |_s| { - ev.send(Event::Command(cmd.clone())); - }); -} - fn main() { std::env::set_var("RUST_LOG", "ncspot=trace"); std::env::set_var("RUST_BACKTRACE", "full"); @@ -131,7 +118,6 @@ fn main() { cursive.set_theme(theme::default()); let event_manager = EventManager::new(cursive.cb_sink().clone()); - let mut cmd_manager = CommandManager::new(); let spotify = Arc::new(spotify::Spotify::new( event_manager.clone(), @@ -140,13 +126,13 @@ fn main() { config::CLIENT_ID.to_string(), )); - let queue = Arc::new(Mutex::new(queue::Queue::new( + let queue = Arc::new(queue::Queue::new( event_manager.clone(), spotify.clone(), - ))); + )); #[cfg(feature = "mpris")] - let mpris_manager = mpris::MprisManager::new(spotify.clone(), queue.clone()); + let mpris_manager = Arc::new(mpris::MprisManager::new(spotify.clone(), queue.clone())); let search = ui::search::SearchView::new(spotify.clone(), queue.clone()); @@ -167,23 +153,19 @@ fn main() { }); } - let mut playlists_view = ui::playlist::PlaylistView::new(&playlists, queue.clone()); + let playlistsview = ui::playlist::PlaylistView::new(&playlists, queue.clone()); - let mut queueview = ui::queue::QueueView::new(queue.clone()); + let queueview = ui::queue::QueueView::new(queue.clone()); let logview_scroller = ScrollView::new(logview).scroll_strategy(ScrollStrategy::StickToBottom); let status = ui::statusbar::StatusBar::new(queue.clone(), spotify.clone()); let mut layout = ui::layout::Layout::new(status, &event_manager) - .view("search", search.view.with_id("search"), "Search") + .view("search", search.with_id("search"), "Search") .view("log", logview_scroller, "Log") - .view( - "playlists", - playlists_view.view.take().unwrap(), - "Playlists", - ) - .view("queue", queueview.view.take().unwrap(), "Queue"); + .view("playlists", playlistsview, "Playlists") + .view("queue", queueview, "Queue"); // initial view is queue layout.set_view("queue"); @@ -206,151 +188,20 @@ fn main() { cursive.add_fullscreen_layer(layout.with_id("main")); - // Register commands - cmd_manager.register( - "quit", - vec!["q", "x"], - Box::new(move |s, _args| { - s.quit(); - Ok(None) - }), - ); + let mut cmd_manager = CommandManager::new(); + cmd_manager.register_all(spotify.clone(), queue.clone()); + #[cfg(feature = "mpris")] { - let queue = queue.clone(); - cmd_manager.register( - "toggleplayback", - vec!["toggleplay", "toggle", "play", "pause"], - Box::new(move |_s, _args| { - queue.lock().expect("could not lock queue").toggleplayback(); - Ok(None) - }), - ); + let mpris_manager = mpris_manager.clone(); + cmd_manager.register_callback(Box::new(move || { + mpris_manager.update(); + })); } - { - let queue = queue.clone(); - cmd_manager.register( - "stop", - Vec::new(), - Box::new(move |_s, _args| { - queue.lock().expect("could not lock queue").stop(); - Ok(None) - }), - ); - } + let cmd_manager = Arc::new(cmd_manager); - { - let queue = queue.clone(); - cmd_manager.register( - "previous", - Vec::new(), - Box::new(move |_s, _args| { - queue.lock().expect("could not lock queue").previous(); - Ok(None) - }), - ); - } - - { - let queue = queue.clone(); - cmd_manager.register( - "next", - Vec::new(), - Box::new(move |_s, _args| { - queue.lock().expect("could not lock queue").next(); - Ok(None) - }), - ); - } - - { - let queue = queue.clone(); - cmd_manager.register( - "clear", - Vec::new(), - Box::new(move |_s, _args| { - queue.lock().expect("could not lock queue").clear(); - Ok(None) - }), - ); - } - - { - cmd_manager.register( - "queue", - Vec::new(), - Box::new(move |s, _args| { - s.call_on_id("main", |v: &mut ui::layout::Layout| { - v.set_view("queue"); - }); - Ok(None) - }), - ); - } - - { - cursive.add_global_callback(Key::F1, move |s| { - s.call_on_id("main", |v: &mut ui::layout::Layout| { - v.set_view("queue"); - }); - }); - } - - cmd_manager.register( - "search", - Vec::new(), - Box::new(move |s, args| { - s.call_on_id("main", |v: &mut ui::layout::Layout| { - v.set_view("search"); - }); - s.call_on_id("search", |v: &mut LinearLayout| { - v.focus_view(&Selector::Id("search_edit")).unwrap(); - }); - if args.len() >= 1 { - s.call_on_id("search_edit", |v: &mut EditView| { - v.set_content(args.join(" ")); - }); - } - Ok(None) - }), - ); - - { - cmd_manager.register( - "playlists", - vec!["lists"], - Box::new(move |s, _args| { - s.call_on_id("main", |v: &mut ui::layout::Layout| { - v.set_view("playlists"); - }); - Ok(None) - }), - ); - } - - cmd_manager.register( - "log", - Vec::new(), - Box::new(move |s, _args| { - s.call_on_id("main", |v: &mut ui::layout::Layout| { - v.set_view("log"); - }); - Ok(None) - }), - ); - - register_keybinding(&mut cursive, &event_manager, 'q', "quit"); - register_keybinding(&mut cursive, &event_manager, 'P', "toggle"); - register_keybinding(&mut cursive, &event_manager, 'S', "stop"); - register_keybinding(&mut cursive, &event_manager, '<', "previous"); - register_keybinding(&mut cursive, &event_manager, '>', "next"); - register_keybinding(&mut cursive, &event_manager, 'c', "clear"); - - register_keybinding(&mut cursive, &event_manager, Key::F1, "queue"); - register_keybinding(&mut cursive, &event_manager, Key::F2, "search"); - register_keybinding(&mut cursive, &event_manager, Key::F3, "playlists"); - register_keybinding(&mut cursive, &event_manager, Key::F9, "log"); + CommandManager::register_keybindings(cmd_manager.clone(), &mut cursive, cfg.keybindings); // cursive event loop while cursive.is_running() { @@ -360,30 +211,18 @@ fn main() { match event { Event::Player(state) => { if state == PlayerEvent::FinishedTrack { - queue.lock().expect("could not lock queue").next(); + queue.next(); } spotify.update_status(state); #[cfg(feature = "mpris")] mpris_manager.update(); } - Event::Playlist(event) => playlists_view.handle_ev(&mut cursive, event), + Event::Playlist(_event) => (), Event::Command(cmd) => { - // TODO: handle non-error output as well - if let Err(e) = cmd_manager.handle(&mut cursive, cmd) { - cursive.call_on_id("main", |v: &mut ui::layout::Layout| { - v.set_error(e); - }); - } - - #[cfg(feature = "mpris")] - mpris_manager.update(); + cmd_manager.handle(&mut cursive, cmd); } - Event::ScreenChange(name) => match name.as_ref() { - "playlists" => playlists_view.repopulate(&mut cursive), - "queue" => queueview.repopulate(&mut cursive), - _ => (), - }, + Event::ScreenChange(_name) => (), } } } diff --git a/src/mpris.rs b/src/mpris.rs index b62a21c..73a51ea 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::rc::Rc; -use std::sync::{mpsc, Arc, Mutex}; +use std::sync::{mpsc, Arc}; use dbus::arg::{RefArg, Variant}; use dbus::stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged; @@ -19,11 +19,11 @@ fn get_playbackstatus(spotify: Arc) -> String { .to_string() } -fn get_metadata(queue: Arc>) -> HashMap>> { +fn get_metadata(queue: Arc) -> HashMap>> { let mut hm: HashMap>> = HashMap::new(); - let queue = queue.lock().expect("could not lock queue"); - let track = queue.get_current(); + let t = queue.get_current(); + let track = t.as_ref(); // TODO hm.insert( "mpris:trackid".to_string(), @@ -86,7 +86,7 @@ fn get_metadata(queue: Arc>) -> HashMap hm } -fn run_dbus_server(spotify: Arc, queue: Arc>, rx: mpsc::Receiver<()>) { +fn run_dbus_server(spotify: Arc, queue: Arc, rx: mpsc::Receiver<()>) { let conn = Rc::new( dbus::Connection::get_private(dbus::BusType::Session).expect("Failed to connect to dbus"), ); @@ -322,7 +322,7 @@ fn run_dbus_server(spotify: Arc, queue: Arc>, rx: mpsc::Re let method_next = { let queue = queue.clone(); f.method("Next", (), move |m| { - queue.lock().expect("failed to lock queue").next(); + queue.next(); Ok(vec![m.msg.method_return()]) }) }; @@ -330,7 +330,7 @@ fn run_dbus_server(spotify: Arc, queue: Arc>, rx: mpsc::Re let method_previous = { let queue = queue.clone(); f.method("Previous", (), move |m| { - queue.lock().expect("failed to lock queue").previous(); + queue.previous(); Ok(vec![m.msg.method_return()]) }) }; @@ -397,12 +397,13 @@ fn run_dbus_server(spotify: Arc, queue: Arc>, rx: mpsc::Re } } +#[derive(Clone)] pub struct MprisManager { tx: mpsc::Sender<()>, } impl MprisManager { - pub fn new(spotify: Arc, queue: Arc>) -> Self { + pub fn new(spotify: Arc, queue: Arc) -> Self { let (tx, rx) = mpsc::channel::<()>(); std::thread::spawn(move || { diff --git a/src/playlists.rs b/src/playlists.rs index c7e0ae3..219d8ee 100644 --- a/src/playlists.rs +++ b/src/playlists.rs @@ -5,8 +5,10 @@ use std::sync::{Arc, RwLock}; use rspotify::spotify::model::playlist::SimplifiedPlaylist; use events::{Event, EventManager}; +use queue::Queue; use spotify::Spotify; use track::Track; +use traits::ListItem; #[derive(Clone, Deserialize, Serialize)] pub struct Playlist { @@ -25,15 +27,36 @@ pub struct PlaylistStore { #[derive(Clone)] pub struct Playlists { - pub store: Arc>, + pub store: Arc>>, ev: EventManager, spotify: Arc, } +impl ListItem for Playlist { + fn is_playing(&self, queue: Arc) -> bool { + let playing: Vec = queue.queue + .read() + .unwrap() + .iter() + .map(|t| t.id.clone()) + .collect(); + let ids: Vec = self.tracks.iter().map(|t| t.id.clone()).collect(); + ids.len() > 0 && playing == ids + } + + fn display_left(&self) -> String { + self.meta.name.clone() + } + + fn display_right(&self) -> String { + format!("{} tracks", self.tracks.len()) + } +} + impl Playlists { pub fn new(ev: &EventManager, spotify: &Arc) -> Playlists { Playlists { - store: Arc::new(RwLock::new(PlaylistStore::default())), + store: Arc::new(RwLock::new(Vec::new())), ev: ev.clone(), spotify: spotify.clone(), } @@ -51,8 +74,8 @@ impl Playlists { if let Ok(cache) = parsed { debug!("playlist cache loaded ({} lists)", cache.playlists.len()); let mut store = self.store.write().expect("can't writelock playlist store"); - store.playlists.clear(); - store.playlists.extend(cache.playlists); + store.clear(); + store.extend(cache.playlists); // force refresh of UI (if visible) self.ev.send(Event::ScreenChange("playlists".to_owned())); @@ -104,11 +127,11 @@ impl Playlists { } fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool { - for local in &self + for local in self .store .read() .expect("can't readlock playlists") - .playlists + .iter() { if local.meta.id == remote.id { return local.meta.snapshot_id != remote.snapshot_id; @@ -119,14 +142,14 @@ impl Playlists { fn append_or_update(&self, updated: &Playlist) -> usize { let mut store = self.store.write().expect("can't writelock playlists"); - for (index, mut local) in store.playlists.iter_mut().enumerate() { + for (index, mut local) in store.iter_mut().enumerate() { if local.meta.id == updated.meta.id { *local = updated.clone(); return index; } } - store.playlists.push(updated.clone()); - store.playlists.len() - 1 + store.push(updated.clone()); + store.len() - 1 } pub fn fetch_playlists(&self) { diff --git a/src/queue.rs b/src/queue.rs index b177a23..dc6ac4f 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -1,14 +1,12 @@ -use std::slice::Iter; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use events::{Event, EventManager}; use spotify::Spotify; use track::Track; pub struct Queue { - // TODO: put this in an RwLock instead of locking the whole Queue struct - queue: Vec, - current_track: Option, + pub queue: Arc>>, + current_track: RwLock>, spotify: Arc, ev: EventManager, } @@ -16,18 +14,18 @@ pub struct Queue { impl Queue { pub fn new(ev: EventManager, spotify: Arc) -> Queue { Queue { - queue: Vec::new(), - current_track: None, + queue: Arc::new(RwLock::new(Vec::new())), + current_track: RwLock::new(None), spotify: spotify, ev: ev, } } pub fn next_index(&self) -> Option { - match self.current_track { + match *self.current_track.read().unwrap() { Some(index) => { let next_index = index + 1; - if next_index < self.queue.len() { + if next_index < self.queue.read().unwrap().len() { Some(next_index) } else { None @@ -38,7 +36,7 @@ impl Queue { } pub fn previous_index(&self) -> Option { - match self.current_track { + match *self.current_track.read().unwrap() { Some(index) => { if index > 0 { Some(index - 1) @@ -50,33 +48,41 @@ impl Queue { } } - pub fn get_current(&self) -> Option<&Track> { - match self.current_track { - Some(index) => Some(&self.queue[index]), + pub fn get_current(&self) -> Option { + match *self.current_track.read().unwrap() { + Some(index) => Some(self.queue.read().unwrap()[index].clone()), None => None, } } - pub fn append(&mut self, track: &Track) { - self.queue.push(track.clone()); + pub fn append(&self, track: &Track) { + let mut q = self.queue.write().unwrap(); + q.push(track.clone()); } - pub fn append_next(&mut self, track: &Track) -> usize { - if let Some(next_index) = self.next_index() { - self.queue.insert(next_index, track.clone()); + pub fn append_next(&self, track: &Track) -> usize { + let next = self.next_index(); + let mut q = self.queue.write().unwrap(); + + if let Some(next_index) = next { + q.insert(next_index, track.clone()); next_index } else { - self.queue.push(track.clone()); - self.queue.len() - 1 + q.push(track.clone()); + q.len() - 1 } } - pub fn remove(&mut self, index: usize) { - self.queue.remove(index); + pub fn remove(&self, index: usize) { + { + let mut q = self.queue.write().unwrap(); + q.remove(index); + } // if the queue is empty or we are at the end of the queue, stop // playback - if self.queue.len() == 0 || index == self.queue.len() { + let len = self.queue.read().unwrap().len(); + if len == 0 || index == len { self.stop(); return; } @@ -84,27 +90,32 @@ impl Queue { // if we are deleting the currently playing track, play the track with // the same index again, because the next track is now at the position // of the one we deleted - if let Some(current_track) = self.current_track { + let current = *self.current_track.read().unwrap(); + if let Some(current_track) = current { if index == current_track { self.play(index); } else if index < current_track { - self.current_track = Some(current_track - 1); + let mut current = self.current_track.write().unwrap(); + current.replace(current_track - 1); } } } - pub fn clear(&mut self) { + pub fn clear(&self) { self.stop(); - self.queue.clear(); + + let mut q = self.queue.write().unwrap(); + q.clear(); // redraw queue if open self.ev.send(Event::ScreenChange("queue".to_owned())); } - pub fn play(&mut self, index: usize) { - let track = &self.queue[index]; + pub fn play(&self, index: usize) { + let track = &self.queue.read().unwrap()[index]; self.spotify.load(&track); - self.current_track = Some(index); + let mut current = self.current_track.write().unwrap(); + current.replace(index); self.spotify.play(); self.spotify.update_track(); } @@ -113,12 +124,13 @@ impl Queue { self.spotify.toggleplayback(); } - pub fn stop(&mut self) { - self.current_track = None; + pub fn stop(&self) { + let mut current = self.current_track.write().unwrap(); + *current = None; self.spotify.stop(); } - pub fn next(&mut self) { + pub fn next(&self) { if let Some(index) = self.next_index() { self.play(index); } else { @@ -126,15 +138,11 @@ impl Queue { } } - pub fn previous(&mut self) { + pub fn previous(&self) { if let Some(index) = self.previous_index() { self.play(index); } else { self.spotify.stop(); } } - - pub fn iter(&self) -> Iter { - self.queue.iter() - } } diff --git a/src/track.rs b/src/track.rs index c4f6529..c337505 100644 --- a/src/track.rs +++ b/src/track.rs @@ -1,7 +1,11 @@ use std::fmt; +use std::sync::Arc; use rspotify::spotify::model::track::FullTrack; +use queue::Queue; +use traits::ListItem; + #[derive(Clone, Deserialize, Serialize)] pub struct Track { pub id: String, @@ -73,3 +77,18 @@ impl fmt::Debug for Track { ) } } + +impl ListItem for Track { + fn is_playing(&self, queue: Arc) -> bool { + let current = queue.get_current(); + current.map(|t| t.id == self.id).unwrap_or(false) + } + + fn display_left(&self) -> String { + format!("{}", self) + } + + fn display_right(&self) -> String { + self.duration_str() + } +} diff --git a/src/traits.rs b/src/traits.rs new file mode 100644 index 0000000..3512552 --- /dev/null +++ b/src/traits.rs @@ -0,0 +1,9 @@ +use std::sync::Arc; + +use queue::Queue; + +pub trait ListItem { + fn is_playing(&self, queue: Arc) -> bool; + fn display_left(&self) -> String; + fn display_right(&self) -> String; +} diff --git a/src/ui/listview.rs b/src/ui/listview.rs new file mode 100644 index 0000000..8461730 --- /dev/null +++ b/src/ui/listview.rs @@ -0,0 +1,97 @@ +use std::cmp::{min, max}; +use std::sync::{Arc, RwLock}; + +use cursive::align::HAlign; +use cursive::theme::ColorStyle; +use cursive::traits::View; +use cursive::{Printer, Vec2}; +use unicode_width::UnicodeWidthStr; + +use queue::Queue; +use traits::ListItem; + +pub struct ListView { + content: Arc>>, + selected: Option, + queue: Arc, +} + +impl ListView { + pub fn new(content: Arc>>, queue: Arc) -> Self { + Self { + content: content, + selected: None, + queue: queue, + } + } + + pub fn with_selected(&self, cb: Box ()>) { + if let Some(i) = self.selected { + if let Some(x) = self.content.read().unwrap().get(i) { + cb(x); + } + } + } + + pub fn get_selected_index(&self) -> Option { + self.selected + } + + pub fn move_focus(&mut self, delta: i32) { + let len = self.content.read().unwrap().len() as i32; + + let new = if let Some(i) = self.selected { + i as i32 + } else { + if delta < 0 { len } else { -1 } + }; + + let new = min(max(new + delta, 0), len - 1); + + self.selected = Some(new as usize); + } +} + +impl View for ListView { + fn draw(&self, printer: &Printer<'_, '_>) { + for (i, item) in self.content.read().unwrap().iter().enumerate() { + let style = if self.selected.is_some() && self.selected.unwrap_or(0) == i { + ColorStyle::highlight() + } else if item.is_playing(self.queue.clone()) { + ColorStyle::secondary() + } else { + ColorStyle::primary() + }; + + let left = item.display_left(); + let right = item.display_right(); + + // draw left string + printer.with_color(style, |printer| { + printer.print((0, i), &left); + }); + + // draw ".." to indicate a cut off string + let max_length = printer.size.x + .checked_sub(right.width() + 1) + .unwrap_or(0); + if max_length < left.width() { + let offset = max_length.checked_sub(1).unwrap_or(0); + printer.with_color(style, |printer| { + printer.print((offset, i), ".."); + }); + } + + // draw right string + let offset = HAlign::Right.get_offset(right.width(), printer.size.x); + + printer.with_color(style, |printer| { + printer.print((offset, i), &right); + }); + } + } + + fn required_size(&mut self, constraint: Vec2) -> Vec2 { + Vec2::new(constraint.x, self.content.read().unwrap().len()) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2da35d0..ac9e5e4 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,6 +2,5 @@ pub mod layout; pub mod playlist; pub mod queue; pub mod search; -pub mod splitbutton; pub mod statusbar; -pub mod trackbutton; +pub mod listview; diff --git a/src/ui/playlist.rs b/src/ui/playlist.rs index 5865810..6105784 100644 --- a/src/ui/playlist.rs +++ b/src/ui/playlist.rs @@ -1,108 +1,28 @@ -use std::sync::{Arc, Mutex}; +use std::sync::Arc; -use cursive::direction::Orientation; -use cursive::event::Key; -use cursive::traits::Boxable; use cursive::traits::Identifiable; -use cursive::views::*; -use cursive::Cursive; +use cursive::view::{ViewWrapper}; +use cursive::views::{IdView, ScrollView}; -use playlists::{Playlist, PlaylistEvent, Playlists}; +use playlists::{Playlist, Playlists}; use queue::Queue; -use ui::splitbutton::SplitButton; +use ui::listview::ListView; pub struct PlaylistView { - pub view: Option>>>, - queue: Arc>, - playlists: Playlists, + list: ScrollView>> } impl PlaylistView { - pub fn new(playlists: &Playlists, queue: Arc>) -> PlaylistView { - let playlists_view = LinearLayout::new(Orientation::Vertical).with_id("playlists"); - let scrollable = ScrollView::new(playlists_view).full_screen(); + pub fn new(playlists: &Playlists, queue: Arc) -> PlaylistView { + let list = ListView::new(playlists.store.clone(), queue).with_id("list"); + let scrollable = ScrollView::new(list); PlaylistView { - view: Some(scrollable), - queue: queue, - playlists: playlists.clone(), - } - } - - fn create_button(&self, playlist: &Playlist) -> SplitButton { - let trackcount = format!("{} tracks", playlist.tracks.len()); - let mut button = SplitButton::new(&playlist.meta.name, &trackcount); - - // plays the selected playlist - { - let queue_ref = self.queue.clone(); - let playlist = playlist.clone(); - button.add_callback(Key::Enter, move |_s| { - let mut locked_queue = queue_ref.lock().expect("could not acquire lock"); - let mut first_played = false; - for track in playlist.tracks.iter() { - let index = locked_queue.append_next(track); - if !first_played { - locked_queue.play(index); - first_played = true; - } - } - }); - } - - // queues the selected playlist - { - let queue_ref = self.queue.clone(); - let playlist = playlist.clone(); - button.add_callback(' ', move |_s| { - let mut locked_queue = queue_ref.lock().expect("could not acquire lock"); - for track in playlist.tracks.iter() { - locked_queue.append(track); - } - }); - } - - button - } - - fn clear_playlists(&self, playlists: &mut ViewRef) { - while playlists.len() > 0 { - playlists.remove_child(0); - } - } - - pub fn repopulate(&self, cursive: &mut Cursive) { - let view_ref: Option> = cursive.find_id("playlists"); - if let Some(mut playlists) = view_ref { - self.clear_playlists(&mut playlists); - - let playlist_store = &self - .playlists - .store - .read() - .expect("can't readlock playlists"); - info!("repopulating {} lists", playlist_store.playlists.len()); - for list in &playlist_store.playlists { - let button = self.create_button(&list); - playlists.add_child(button); - } - } - } - - pub fn handle_ev(&self, cursive: &mut Cursive, event: PlaylistEvent) { - let view_ref: Option> = cursive.find_id("playlists"); - - if let Some(mut playlists) = view_ref { - match event { - PlaylistEvent::NewList(index, list) => { - let button = self.create_button(&list); - - if let Some(_) = playlists.get_child(index) { - playlists.remove_child(index); - } - playlists.insert_child(index, button); - } - } + list: scrollable } } } + +impl ViewWrapper for PlaylistView { + wrap_impl!(self.list: ScrollView>>); +} diff --git a/src/ui/queue.rs b/src/ui/queue.rs index 5cd49b8..3f22daf 100644 --- a/src/ui/queue.rs +++ b/src/ui/queue.rs @@ -1,89 +1,28 @@ -use cursive::direction::Orientation; -use cursive::event::Key; -use cursive::traits::Boxable; use cursive::traits::Identifiable; -use cursive::views::*; -use cursive::Cursive; +use cursive::view::{ViewWrapper}; +use cursive::views::{IdView, ScrollView}; use std::sync::Arc; -use std::sync::Mutex; use queue::Queue; use track::Track; -use ui::splitbutton::SplitButton; -use ui::trackbutton::TrackButton; +use ui::listview::ListView; pub struct QueueView { - pub view: Option>>>, - queue: Arc>, + list: ScrollView>> } impl QueueView { - pub fn new(queue: Arc>) -> QueueView { - let queuelist = LinearLayout::new(Orientation::Vertical).with_id("queue_list"); - let scrollable = ScrollView::new(queuelist).full_screen(); + pub fn new(queue: Arc) -> QueueView { + let list = ListView::new(queue.queue.clone(), queue.clone()).with_id("queue_list"); + let scrollable = ScrollView::new(list); QueueView { - view: Some(scrollable), - queue: queue, - } - } - - fn cb_delete(cursive: &mut Cursive, queue: &mut Queue) { - let view_ref: Option> = cursive.find_id("queue_list"); - if let Some(mut queuelist) = view_ref { - let index = queuelist.get_focus_index(); - queue.remove(index); - queuelist.remove_child(index); - } - } - - fn cb_play(cursive: &mut Cursive, queue: &mut Queue) { - let view_ref: Option> = cursive.find_id("queue_list"); - if let Some(queuelist) = view_ref { - let index = queuelist.get_focus_index(); - queue.play(index); - } - } - - fn create_button(&self, track: &Track) -> SplitButton { - let mut button = TrackButton::new(&track); - // 'd' deletes the selected track - { - let queue_ref = self.queue.clone(); - button.add_callback('d', move |cursive| { - Self::cb_delete( - cursive, - &mut queue_ref.lock().expect("could not lock queue"), - ); - }); - } - - // plays the selected track - { - let queue_ref = self.queue.clone(); - button.add_callback(Key::Enter, move |cursive| { - Self::cb_play( - cursive, - &mut queue_ref.lock().expect("could not lock queue"), - ); - }); - } - button - } - - pub fn repopulate(&self, cursive: &mut Cursive) { - let view_ref: Option> = cursive.find_id("queue_list"); - if let Some(mut queuelist) = view_ref { - while queuelist.len() > 0 { - queuelist.remove_child(0); - } - - let queue = self.queue.lock().expect("could not lock queue"); - for track in queue.iter() { - let button = self.create_button(track); - queuelist.add_child(button); - } + list: scrollable } } } + +impl ViewWrapper for QueueView { + wrap_impl!(self.list: ScrollView>>); +} diff --git a/src/ui/search.rs b/src/ui/search.rs index c0f520d..3d88581 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -1,81 +1,120 @@ +#![allow(unused_imports)] + use cursive::direction::Orientation; -use cursive::event::Key; -use cursive::traits::Boxable; -use cursive::traits::Identifiable; -use cursive::views::*; -use cursive::Cursive; -use std::sync::Arc; -use std::sync::Mutex; +use cursive::event::{AnyCb, Event, EventResult, Key}; +use cursive::traits::{Boxable, Identifiable, Finder, View}; +use cursive::view::{Selector, ViewWrapper}; +use cursive::views::{EditView, IdView, ScrollView, ViewRef}; +use cursive::{Cursive, Printer, Vec2}; +use std::cell::RefCell; +use std::sync::{Arc, Mutex, RwLock}; use queue::Queue; use spotify::Spotify; use track::Track; -use ui::trackbutton::TrackButton; +use ui::listview::ListView; pub struct SearchView { - pub view: LinearLayout, + results: Arc>>, + edit: IdView, + list: ScrollView>>, + edit_focused: bool, } impl SearchView { - fn search_handler( - s: &mut Cursive, - input: &str, - spotify: Arc, - queue: Arc>, - ) { - let mut results: ViewRef = s.find_id("search_results").unwrap(); - let tracks = spotify.search(input, 50, 0); + pub fn new(spotify: Arc, queue: Arc) -> SearchView { + let results = Arc::new(RwLock::new(Vec::new())); - results.clear(); - - if let Ok(tracks) = tracks { - for search_track in tracks.tracks.items { - let track = Track::new(&search_track); - let mut button = TrackButton::new(&track); - - // plays the selected track - { - let queue = queue.clone(); - let track = track.clone(); - button.add_callback(Key::Enter, move |_cursive| { - let mut queue = queue.lock().unwrap(); - let index = queue.append_next(&track); - queue.play(index); - }); - } - - // queues the selected track - { - let queue = queue.clone(); - let track = track.clone(); - button.add_callback(' ', move |_cursive| { - let mut queue = queue.lock().unwrap(); - queue.append(&track); - }); - } - - results.add_child("", button); - } - } - } - - pub fn new(spotify: Arc, queue: Arc>) -> SearchView { - let queue_ref = queue.clone(); let searchfield = EditView::new() .on_submit(move |s, input| { if input.len() > 0 { - Self::search_handler(s, input, spotify.clone(), queue_ref.clone()); + s.call_on_id("search", |v: &mut SearchView| { + v.run_search(input, spotify.clone()); + v.focus_view(&Selector::Id("list")).unwrap(); + }); } }) - .with_id("search_edit") - .full_width() - .fixed_height(1); - let results = ListView::new().with_id("search_results").full_width(); - let scrollable = ScrollView::new(results).full_width().full_height(); - let layout = LinearLayout::new(Orientation::Vertical) - .child(searchfield) - .child(scrollable); + .with_id("search_edit"); + let list = ListView::new(results.clone(), queue).with_id("list"); + let scrollable = ScrollView::new(list); - return SearchView { view: layout }; + SearchView { + results: results, + edit: searchfield, + list: scrollable, + edit_focused: false + } + } + + pub fn run_search>(&mut self, query: S, spotify: Arc) { + let query = query.into(); + let q = query.clone(); + self.edit.call_on(&Selector::Id("search_edit"), |v: &mut EditView| { + v.set_content(q); + }); + + if let Ok(results) = spotify.search(&query, 50, 0) { + let tracks = results.tracks.items.iter().map(|ft| Track::new(ft)).collect(); + let mut r = self.results.write().unwrap(); + *r = tracks; + self.edit_focused = false; + } + } + + pub fn focus_search(&mut self) { + self.edit.call_on(&Selector::Id("search_edit"), |v: &mut EditView| { + v.set_content(""); + }); + self.edit_focused = true; + } +} + +impl View for SearchView { + fn draw(&self, printer: &Printer<'_, '_>) { + { + let printer = &printer + .offset((0, 0)) + .cropped((printer.size.x, 1)) + .focused(self.edit_focused); + self.edit.draw(printer); + } + + let printer = &printer + .offset((0, 1)) + .cropped((printer.size.x, printer.size.y - 1)) + .focused(!self.edit_focused); + self.list.draw(printer); + } + + fn layout(&mut self, size: Vec2) { + self.edit.layout(Vec2::new(size.x, 1)); + self.list.layout(Vec2::new(size.x, size.y - 1)); + } + + fn call_on_any<'a>(&mut self, selector: &Selector<'_>, mut callback: AnyCb<'a>) { + self.edit.call_on_any(selector, Box::new(|v| callback(v))); + self.list.call_on_any(selector, Box::new(|v| callback(v))); + } + + fn focus_view(&mut self, selector: &Selector<'_>) -> Result<(), ()> { + if let Selector::Id(s) = selector { + self.edit_focused = s == &"search_edit"; + Ok(()) + } else { + Err(()) + } + } + + fn on_event(&mut self, event: Event) -> EventResult { + if self.edit_focused { + if event == Event::Key(Key::Esc) { + self.edit_focused = false; + EventResult::Consumed(None) + } else { + self.edit.on_event(event) + } + } else { + self.list.on_event(event) + } } } diff --git a/src/ui/splitbutton.rs b/src/ui/splitbutton.rs deleted file mode 100644 index fd0d3dc..0000000 --- a/src/ui/splitbutton.rs +++ /dev/null @@ -1,111 +0,0 @@ -use cursive::align::HAlign; -use cursive::direction::Direction; -use cursive::event::{Callback, Event, EventResult, EventTrigger}; -use cursive::theme::ColorStyle; -use cursive::traits::View; -use cursive::vec::Vec2; -use cursive::Cursive; -use cursive::Printer; -use unicode_width::UnicodeWidthStr; - -pub struct SplitButton { - callbacks: Vec<(EventTrigger, Callback)>, - - left: String, - right: String, - - enabled: bool, - last_size: Vec2, - invalidated: bool, -} - -impl SplitButton { - pub fn new(left: &str, right: &str) -> SplitButton { - SplitButton { - callbacks: Vec::new(), - left: left.to_owned(), - right: right.to_owned(), - enabled: true, - last_size: Vec2::zero(), - invalidated: true, - } - } - - pub fn add_callback(&mut self, trigger: E, cb: F) - where - E: Into, - F: 'static + Fn(&mut Cursive), - { - self.callbacks.push((trigger.into(), Callback::from_fn(cb))); - } -} - -// This is heavily based on Cursive's Button implementation with minor -// modifications to print the track's duration at the right screen border -impl View for SplitButton { - fn draw(&self, printer: &Printer<'_, '_>) { - if printer.size.x == 0 { - return; - } - - let style = if !(self.enabled && printer.enabled) { - ColorStyle::secondary() - } else if !printer.focused { - ColorStyle::primary() - } else { - ColorStyle::highlight() - }; - - // draw left string - printer.with_color(style, |printer| { - printer.print((0, 0), &self.left); - }); - - // draw ".." to indicate a cut off string - let max_length = printer - .size - .x - .checked_sub(self.right.width() + 1) - .unwrap_or(0); - if max_length < self.left.width() { - let offset = max_length.checked_sub(1).unwrap_or(0); - printer.with_color(style, |printer| { - printer.print((offset, 0), ".."); - }); - } - - // draw right string - let offset = HAlign::Right.get_offset(self.right.width(), printer.size.x); - - printer.with_color(style, |printer| { - printer.print((offset, 0), &self.right); - }); - } - - fn on_event(&mut self, event: Event) -> EventResult { - for (trigger, callback) in self.callbacks.iter() { - if trigger.apply(&event) { - return EventResult::Consumed(Some(callback.clone())); - } - } - EventResult::Ignored - } - - fn layout(&mut self, size: Vec2) { - self.last_size = size; - self.invalidated = false; - } - - fn required_size(&mut self, constraint: Vec2) -> Vec2 { - // we always want the full width - Vec2::new(constraint.x, 1) - } - - fn take_focus(&mut self, _: Direction) -> bool { - self.enabled - } - - fn needs_relayout(&self) -> bool { - self.invalidated - } -} diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index 7f65cf7..1e26dcd 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, Mutex}; +use std::sync::{Arc}; use cursive::align::HAlign; use cursive::theme::ColorStyle; @@ -11,12 +11,12 @@ use queue::Queue; use spotify::{PlayerEvent, Spotify}; pub struct StatusBar { - queue: Arc>, + queue: Arc, spotify: Arc, } impl StatusBar { - pub fn new(queue: Arc>, spotify: Arc) -> StatusBar { + pub fn new(queue: Arc, spotify: Arc) -> StatusBar { StatusBar { queue: queue, spotify: spotify, @@ -55,12 +55,7 @@ impl View for StatusBar { printer.print((0, 1), &state_icon); }); - if let Some(ref t) = self - .queue - .lock() - .expect("could not lock queue") - .get_current() - { + if let Some(ref t) = self.queue.get_current() { let elapsed = self.spotify.get_current_progress(); let formatted_elapsed = format!( "{:02}:{:02}", diff --git a/src/ui/trackbutton.rs b/src/ui/trackbutton.rs deleted file mode 100644 index fdc2d01..0000000 --- a/src/ui/trackbutton.rs +++ /dev/null @@ -1,11 +0,0 @@ -use track::Track; -use ui::splitbutton::SplitButton; - -pub struct TrackButton {} - -impl TrackButton { - pub fn new(track: &Track) -> SplitButton { - let button = SplitButton::new(&track.to_string(), &track.duration_str()); - button - } -}