Add rebindable keys, refactor lists

This commit is contained in:
KoffeinFlummi
2019-03-17 03:17:30 +01:00
parent 3d385aff9b
commit 5a85619105
16 changed files with 757 additions and 605 deletions

View File

@@ -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<String, Box<dyn Fn(&mut Cursive, Vec<String>) -> Result<Option<String>, String>>>,
aliases: HashMap<String, String>,
callbacks: Vec<Box<dyn Fn() -> ()>>
}
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<Spotify>, queue: Arc<Queue>) {
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<Track>| {
v.move_focus(dir * amount);
});
s.call_on_id("list", |v: &mut ListView<Track>| {
v.move_focus(dir * amount);
});
s.call_on_id("list", |v: &mut ListView<Playlist>| {
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<Track>| {
v.with_selected(Box::new(move |t| {
queue.append(t);
}));
});
}
{
let queue = queue.clone();
s.call_on_id("list", |v: &mut ListView<Playlist>| {
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<Track>| {
v.get_selected_index().map(|i| queue.play(i));
});
}
{
let queue = queue.clone();
s.call_on_id("list", |v: &mut ListView<Track>| {
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<Playlist>| {
v.with_selected(Box::new(move |pl| {
let indices: Vec<usize> = 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<Track>| {
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<Option<String>, String> {
pub fn handle(&self, s: &mut Cursive, cmd: String) {
let components: Vec<String> = 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<dyn Fn() -> ()>) {
self.callbacks.push(cb);
}
pub fn register_keybinding<'a, E: Into<cursive::event::Event>, S: Into<String>>(
this: Arc<Self>,
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<Self>,
cursive: &'a mut Cursive,
keybindings: Option<HashMap<String, String>>
) {
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<String, String> {
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())
}
}
}
}

View File

@@ -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<HashMap<String, String>>,
}

View File

@@ -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<E: Into<cursive::event::Event>, S: Into<String>>(
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) => (),
}
}
}

View File

@@ -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<Spotify>) -> String {
.to_string()
}
fn get_metadata(queue: Arc<Mutex<Queue>>) -> HashMap<String, Variant<Box<RefArg>>> {
fn get_metadata(queue: Arc<Queue>) -> HashMap<String, Variant<Box<RefArg>>> {
let mut hm: HashMap<String, Variant<Box<RefArg>>> = 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<Mutex<Queue>>) -> HashMap<String, Variant<Box<RefArg>
hm
}
fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Receiver<()>) {
fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Queue>, 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<Spotify>, queue: Arc<Mutex<Queue>>, 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<Spotify>, queue: Arc<Mutex<Queue>>, 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<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Re
}
}
#[derive(Clone)]
pub struct MprisManager {
tx: mpsc::Sender<()>,
}
impl MprisManager {
pub fn new(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>) -> Self {
pub fn new(spotify: Arc<Spotify>, queue: Arc<Queue>) -> Self {
let (tx, rx) = mpsc::channel::<()>();
std::thread::spawn(move || {

View File

@@ -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<RwLock<PlaylistStore>>,
pub store: Arc<RwLock<Vec<Playlist>>>,
ev: EventManager,
spotify: Arc<Spotify>,
}
impl ListItem for Playlist {
fn is_playing(&self, queue: Arc<Queue>) -> bool {
let playing: Vec<String> = queue.queue
.read()
.unwrap()
.iter()
.map(|t| t.id.clone())
.collect();
let ids: Vec<String> = 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<Spotify>) -> 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) {

View File

@@ -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<Track>,
current_track: Option<usize>,
pub queue: Arc<RwLock<Vec<Track>>>,
current_track: RwLock<Option<usize>>,
spotify: Arc<Spotify>,
ev: EventManager,
}
@@ -16,18 +14,18 @@ pub struct Queue {
impl Queue {
pub fn new(ev: EventManager, spotify: Arc<Spotify>) -> 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<usize> {
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<usize> {
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<Track> {
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<Track> {
self.queue.iter()
}
}

View File

@@ -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<Queue>) -> 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()
}
}

9
src/traits.rs Normal file
View File

@@ -0,0 +1,9 @@
use std::sync::Arc;
use queue::Queue;
pub trait ListItem {
fn is_playing(&self, queue: Arc<Queue>) -> bool;
fn display_left(&self) -> String;
fn display_right(&self) -> String;
}

97
src/ui/listview.rs Normal file
View File

@@ -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<I: 'static + ListItem> {
content: Arc<RwLock<Vec<I>>>,
selected: Option<usize>,
queue: Arc<Queue>,
}
impl<I: ListItem> ListView<I> {
pub fn new(content: Arc<RwLock<Vec<I>>>, queue: Arc<Queue>) -> Self {
Self {
content: content,
selected: None,
queue: queue,
}
}
pub fn with_selected(&self, cb: Box<Fn(&I) -> ()>) {
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<usize> {
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<I: ListItem> View for ListView<I> {
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())
}
}

View File

@@ -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;

View File

@@ -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<BoxView<ScrollView<IdView<LinearLayout>>>>,
queue: Arc<Mutex<Queue>>,
playlists: Playlists,
list: ScrollView<IdView<ListView<Playlist>>>
}
impl PlaylistView {
pub fn new(playlists: &Playlists, queue: Arc<Mutex<Queue>>) -> 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<Queue>) -> 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);
// <enter> 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;
}
}
});
}
// <space> 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<LinearLayout>) {
while playlists.len() > 0 {
playlists.remove_child(0);
}
}
pub fn repopulate(&self, cursive: &mut Cursive) {
let view_ref: Option<ViewRef<LinearLayout>> = 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<ViewRef<LinearLayout>> = 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<IdView<ListView<Playlist>>>);
}

View File

@@ -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<BoxView<ScrollView<IdView<LinearLayout>>>>,
queue: Arc<Mutex<Queue>>,
list: ScrollView<IdView<ListView<Track>>>
}
impl QueueView {
pub fn new(queue: Arc<Mutex<Queue>>) -> QueueView {
let queuelist = LinearLayout::new(Orientation::Vertical).with_id("queue_list");
let scrollable = ScrollView::new(queuelist).full_screen();
pub fn new(queue: Arc<Queue>) -> 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<ViewRef<LinearLayout>> = 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<ViewRef<LinearLayout>> = 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"),
);
});
}
// <enter> 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<ViewRef<LinearLayout>> = 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<IdView<ListView<Track>>>);
}

View File

@@ -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<RwLock<Vec<Track>>>,
edit: IdView<EditView>,
list: ScrollView<IdView<ListView<Track>>>,
edit_focused: bool,
}
impl SearchView {
fn search_handler(
s: &mut Cursive,
input: &str,
spotify: Arc<Spotify>,
queue: Arc<Mutex<Queue>>,
) {
let mut results: ViewRef<ListView> = s.find_id("search_results").unwrap();
let tracks = spotify.search(input, 50, 0);
pub fn new(spotify: Arc<Spotify>, queue: Arc<Queue>) -> 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);
// <enter> 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);
});
}
// <space> 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<Spotify>, queue: Arc<Mutex<Queue>>) -> 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<S: Into<String>>(&mut self, query: S, spotify: Arc<Spotify>) {
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)
}
}
}

View File

@@ -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<F, E>(&mut self, trigger: E, cb: F)
where
E: Into<EventTrigger>,
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
}
}

View File

@@ -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<Mutex<Queue>>,
queue: Arc<Queue>,
spotify: Arc<Spotify>,
}
impl StatusBar {
pub fn new(queue: Arc<Mutex<Queue>>, spotify: Arc<Spotify>) -> StatusBar {
pub fn new(queue: Arc<Queue>, spotify: Arc<Spotify>) -> 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}",

View File

@@ -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
}
}