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::collections::HashMap;
use std::sync::Arc;
use cursive::Cursive; 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 { pub struct CommandManager {
commands: commands:
HashMap<String, Box<dyn Fn(&mut Cursive, Vec<String>) -> Result<Option<String>, String>>>, HashMap<String, Box<dyn Fn(&mut Cursive, Vec<String>) -> Result<Option<String>, String>>>,
aliases: HashMap<String, String>, aliases: HashMap<String, String>,
callbacks: Vec<Box<dyn Fn() -> ()>>
} }
impl CommandManager { impl CommandManager {
@@ -13,6 +24,7 @@ impl CommandManager {
CommandManager { CommandManager {
commands: HashMap::new(), commands: HashMap::new(),
aliases: HashMap::new(), aliases: HashMap::new(),
callbacks: Vec::new(),
} }
} }
@@ -29,6 +41,265 @@ impl CommandManager {
self.commands.insert(name, cb); 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 { fn handle_aliases(&self, name: &String) -> String {
if let Some(s) = self.aliases.get(name) { if let Some(s) = self.aliases.get(name) {
self.handle_aliases(s) 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(); 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()) cb(s, components[1..].to_vec())
} else { } else {
Err("Unknown command.".to_string()) 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"; pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b";
#[derive(Serialize, Deserialize, Debug, Default)] #[derive(Serialize, Deserialize, Debug, Default)]
pub struct Config { pub struct Config {
pub username: String, pub username: String,
pub password: String, pub password: String,
pub keybindings: Option<HashMap<String, String>>,
} }

View File

@@ -1,4 +1,5 @@
extern crate crossbeam_channel; extern crate crossbeam_channel;
#[macro_use]
extern crate cursive; extern crate cursive;
extern crate failure; extern crate failure;
extern crate futures; extern crate futures;
@@ -28,12 +29,10 @@ use std::io::prelude::*;
use std::path::PathBuf; use std::path::PathBuf;
use std::process; use std::process;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex;
use std::thread; use std::thread;
use cursive::event::Key; use cursive::traits::{Identifiable};
use cursive::traits::{Identifiable, View}; use cursive::view::{ScrollStrategy};
use cursive::view::{ScrollStrategy, Selector};
use cursive::views::*; use cursive::views::*;
use cursive::Cursive; use cursive::Cursive;
@@ -46,6 +45,7 @@ mod spotify;
mod theme; mod theme;
mod track; mod track;
mod ui; mod ui;
mod traits;
#[cfg(feature = "mpris")] #[cfg(feature = "mpris")]
mod 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() { fn main() {
std::env::set_var("RUST_LOG", "ncspot=trace"); std::env::set_var("RUST_LOG", "ncspot=trace");
std::env::set_var("RUST_BACKTRACE", "full"); std::env::set_var("RUST_BACKTRACE", "full");
@@ -131,7 +118,6 @@ fn main() {
cursive.set_theme(theme::default()); cursive.set_theme(theme::default());
let event_manager = EventManager::new(cursive.cb_sink().clone()); let event_manager = EventManager::new(cursive.cb_sink().clone());
let mut cmd_manager = CommandManager::new();
let spotify = Arc::new(spotify::Spotify::new( let spotify = Arc::new(spotify::Spotify::new(
event_manager.clone(), event_manager.clone(),
@@ -140,13 +126,13 @@ fn main() {
config::CLIENT_ID.to_string(), config::CLIENT_ID.to_string(),
)); ));
let queue = Arc::new(Mutex::new(queue::Queue::new( let queue = Arc::new(queue::Queue::new(
event_manager.clone(), event_manager.clone(),
spotify.clone(), spotify.clone(),
))); ));
#[cfg(feature = "mpris")] #[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()); 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 logview_scroller = ScrollView::new(logview).scroll_strategy(ScrollStrategy::StickToBottom);
let status = ui::statusbar::StatusBar::new(queue.clone(), spotify.clone()); let status = ui::statusbar::StatusBar::new(queue.clone(), spotify.clone());
let mut layout = ui::layout::Layout::new(status, &event_manager) 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("log", logview_scroller, "Log")
.view( .view("playlists", playlistsview, "Playlists")
"playlists", .view("queue", queueview, "Queue");
playlists_view.view.take().unwrap(),
"Playlists",
)
.view("queue", queueview.view.take().unwrap(), "Queue");
// initial view is queue // initial view is queue
layout.set_view("queue"); layout.set_view("queue");
@@ -206,151 +188,20 @@ fn main() {
cursive.add_fullscreen_layer(layout.with_id("main")); cursive.add_fullscreen_layer(layout.with_id("main"));
// Register commands let mut cmd_manager = CommandManager::new();
cmd_manager.register( cmd_manager.register_all(spotify.clone(), queue.clone());
"quit",
vec!["q", "x"],
Box::new(move |s, _args| {
s.quit();
Ok(None)
}),
);
#[cfg(feature = "mpris")]
{ {
let queue = queue.clone(); let mpris_manager = mpris_manager.clone();
cmd_manager.register( cmd_manager.register_callback(Box::new(move || {
"toggleplayback", mpris_manager.update();
vec!["toggleplay", "toggle", "play", "pause"], }));
Box::new(move |_s, _args| {
queue.lock().expect("could not lock queue").toggleplayback();
Ok(None)
}),
);
} }
{ let cmd_manager = Arc::new(cmd_manager);
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)
}),
);
}
{ CommandManager::register_keybindings(cmd_manager.clone(), &mut cursive, cfg.keybindings);
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");
// cursive event loop // cursive event loop
while cursive.is_running() { while cursive.is_running() {
@@ -360,30 +211,18 @@ fn main() {
match event { match event {
Event::Player(state) => { Event::Player(state) => {
if state == PlayerEvent::FinishedTrack { if state == PlayerEvent::FinishedTrack {
queue.lock().expect("could not lock queue").next(); queue.next();
} }
spotify.update_status(state); spotify.update_status(state);
#[cfg(feature = "mpris")] #[cfg(feature = "mpris")]
mpris_manager.update(); mpris_manager.update();
} }
Event::Playlist(event) => playlists_view.handle_ev(&mut cursive, event), Event::Playlist(_event) => (),
Event::Command(cmd) => { Event::Command(cmd) => {
// TODO: handle non-error output as well cmd_manager.handle(&mut cursive, cmd);
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();
} }
Event::ScreenChange(name) => match name.as_ref() { Event::ScreenChange(_name) => (),
"playlists" => playlists_view.repopulate(&mut cursive),
"queue" => queueview.repopulate(&mut cursive),
_ => (),
},
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use std::sync::{mpsc, Arc, Mutex}; use std::sync::{mpsc, Arc};
use dbus::arg::{RefArg, Variant}; use dbus::arg::{RefArg, Variant};
use dbus::stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged; use dbus::stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged;
@@ -19,11 +19,11 @@ fn get_playbackstatus(spotify: Arc<Spotify>) -> String {
.to_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 mut hm: HashMap<String, Variant<Box<RefArg>>> = HashMap::new();
let queue = queue.lock().expect("could not lock queue"); let t = queue.get_current();
let track = queue.get_current(); let track = t.as_ref(); // TODO
hm.insert( hm.insert(
"mpris:trackid".to_string(), "mpris:trackid".to_string(),
@@ -86,7 +86,7 @@ fn get_metadata(queue: Arc<Mutex<Queue>>) -> HashMap<String, Variant<Box<RefArg>
hm 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( let conn = Rc::new(
dbus::Connection::get_private(dbus::BusType::Session).expect("Failed to connect to dbus"), 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 method_next = {
let queue = queue.clone(); let queue = queue.clone();
f.method("Next", (), move |m| { f.method("Next", (), move |m| {
queue.lock().expect("failed to lock queue").next(); queue.next();
Ok(vec![m.msg.method_return()]) 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 method_previous = {
let queue = queue.clone(); let queue = queue.clone();
f.method("Previous", (), move |m| { f.method("Previous", (), move |m| {
queue.lock().expect("failed to lock queue").previous(); queue.previous();
Ok(vec![m.msg.method_return()]) 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 { pub struct MprisManager {
tx: mpsc::Sender<()>, tx: mpsc::Sender<()>,
} }
impl MprisManager { 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::<()>(); let (tx, rx) = mpsc::channel::<()>();
std::thread::spawn(move || { std::thread::spawn(move || {

View File

@@ -5,8 +5,10 @@ use std::sync::{Arc, RwLock};
use rspotify::spotify::model::playlist::SimplifiedPlaylist; use rspotify::spotify::model::playlist::SimplifiedPlaylist;
use events::{Event, EventManager}; use events::{Event, EventManager};
use queue::Queue;
use spotify::Spotify; use spotify::Spotify;
use track::Track; use track::Track;
use traits::ListItem;
#[derive(Clone, Deserialize, Serialize)] #[derive(Clone, Deserialize, Serialize)]
pub struct Playlist { pub struct Playlist {
@@ -25,15 +27,36 @@ pub struct PlaylistStore {
#[derive(Clone)] #[derive(Clone)]
pub struct Playlists { pub struct Playlists {
pub store: Arc<RwLock<PlaylistStore>>, pub store: Arc<RwLock<Vec<Playlist>>>,
ev: EventManager, ev: EventManager,
spotify: Arc<Spotify>, 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 { impl Playlists {
pub fn new(ev: &EventManager, spotify: &Arc<Spotify>) -> Playlists { pub fn new(ev: &EventManager, spotify: &Arc<Spotify>) -> Playlists {
Playlists { Playlists {
store: Arc::new(RwLock::new(PlaylistStore::default())), store: Arc::new(RwLock::new(Vec::new())),
ev: ev.clone(), ev: ev.clone(),
spotify: spotify.clone(), spotify: spotify.clone(),
} }
@@ -51,8 +74,8 @@ impl Playlists {
if let Ok(cache) = parsed { if let Ok(cache) = parsed {
debug!("playlist cache loaded ({} lists)", cache.playlists.len()); debug!("playlist cache loaded ({} lists)", cache.playlists.len());
let mut store = self.store.write().expect("can't writelock playlist store"); let mut store = self.store.write().expect("can't writelock playlist store");
store.playlists.clear(); store.clear();
store.playlists.extend(cache.playlists); store.extend(cache.playlists);
// force refresh of UI (if visible) // force refresh of UI (if visible)
self.ev.send(Event::ScreenChange("playlists".to_owned())); self.ev.send(Event::ScreenChange("playlists".to_owned()));
@@ -104,11 +127,11 @@ impl Playlists {
} }
fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool { fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool {
for local in &self for local in self
.store .store
.read() .read()
.expect("can't readlock playlists") .expect("can't readlock playlists")
.playlists .iter()
{ {
if local.meta.id == remote.id { if local.meta.id == remote.id {
return local.meta.snapshot_id != remote.snapshot_id; return local.meta.snapshot_id != remote.snapshot_id;
@@ -119,14 +142,14 @@ impl Playlists {
fn append_or_update(&self, updated: &Playlist) -> usize { fn append_or_update(&self, updated: &Playlist) -> usize {
let mut store = self.store.write().expect("can't writelock playlists"); 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 { if local.meta.id == updated.meta.id {
*local = updated.clone(); *local = updated.clone();
return index; return index;
} }
} }
store.playlists.push(updated.clone()); store.push(updated.clone());
store.playlists.len() - 1 store.len() - 1
} }
pub fn fetch_playlists(&self) { pub fn fetch_playlists(&self) {

View File

@@ -1,14 +1,12 @@
use std::slice::Iter; use std::sync::{Arc, RwLock};
use std::sync::Arc;
use events::{Event, EventManager}; use events::{Event, EventManager};
use spotify::Spotify; use spotify::Spotify;
use track::Track; use track::Track;
pub struct Queue { pub struct Queue {
// TODO: put this in an RwLock instead of locking the whole Queue struct pub queue: Arc<RwLock<Vec<Track>>>,
queue: Vec<Track>, current_track: RwLock<Option<usize>>,
current_track: Option<usize>,
spotify: Arc<Spotify>, spotify: Arc<Spotify>,
ev: EventManager, ev: EventManager,
} }
@@ -16,18 +14,18 @@ pub struct Queue {
impl Queue { impl Queue {
pub fn new(ev: EventManager, spotify: Arc<Spotify>) -> Queue { pub fn new(ev: EventManager, spotify: Arc<Spotify>) -> Queue {
Queue { Queue {
queue: Vec::new(), queue: Arc::new(RwLock::new(Vec::new())),
current_track: None, current_track: RwLock::new(None),
spotify: spotify, spotify: spotify,
ev: ev, ev: ev,
} }
} }
pub fn next_index(&self) -> Option<usize> { pub fn next_index(&self) -> Option<usize> {
match self.current_track { match *self.current_track.read().unwrap() {
Some(index) => { Some(index) => {
let next_index = index + 1; let next_index = index + 1;
if next_index < self.queue.len() { if next_index < self.queue.read().unwrap().len() {
Some(next_index) Some(next_index)
} else { } else {
None None
@@ -38,7 +36,7 @@ impl Queue {
} }
pub fn previous_index(&self) -> Option<usize> { pub fn previous_index(&self) -> Option<usize> {
match self.current_track { match *self.current_track.read().unwrap() {
Some(index) => { Some(index) => {
if index > 0 { if index > 0 {
Some(index - 1) Some(index - 1)
@@ -50,33 +48,41 @@ impl Queue {
} }
} }
pub fn get_current(&self) -> Option<&Track> { pub fn get_current(&self) -> Option<Track> {
match self.current_track { match *self.current_track.read().unwrap() {
Some(index) => Some(&self.queue[index]), Some(index) => Some(self.queue.read().unwrap()[index].clone()),
None => None, None => None,
} }
} }
pub fn append(&mut self, track: &Track) { pub fn append(&self, track: &Track) {
self.queue.push(track.clone()); let mut q = self.queue.write().unwrap();
q.push(track.clone());
} }
pub fn append_next(&mut self, track: &Track) -> usize { pub fn append_next(&self, track: &Track) -> usize {
if let Some(next_index) = self.next_index() { let next = self.next_index();
self.queue.insert(next_index, track.clone()); let mut q = self.queue.write().unwrap();
if let Some(next_index) = next {
q.insert(next_index, track.clone());
next_index next_index
} else { } else {
self.queue.push(track.clone()); q.push(track.clone());
self.queue.len() - 1 q.len() - 1
} }
} }
pub fn remove(&mut self, index: usize) { pub fn remove(&self, index: usize) {
self.queue.remove(index); {
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 // if the queue is empty or we are at the end of the queue, stop
// playback // playback
if self.queue.len() == 0 || index == self.queue.len() { let len = self.queue.read().unwrap().len();
if len == 0 || index == len {
self.stop(); self.stop();
return; return;
} }
@@ -84,27 +90,32 @@ impl Queue {
// if we are deleting the currently playing track, play the track with // 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 // the same index again, because the next track is now at the position
// of the one we deleted // 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 { if index == current_track {
self.play(index); self.play(index);
} else if index < current_track { } 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.stop();
self.queue.clear();
let mut q = self.queue.write().unwrap();
q.clear();
// redraw queue if open // redraw queue if open
self.ev.send(Event::ScreenChange("queue".to_owned())); self.ev.send(Event::ScreenChange("queue".to_owned()));
} }
pub fn play(&mut self, index: usize) { pub fn play(&self, index: usize) {
let track = &self.queue[index]; let track = &self.queue.read().unwrap()[index];
self.spotify.load(&track); 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.play();
self.spotify.update_track(); self.spotify.update_track();
} }
@@ -113,12 +124,13 @@ impl Queue {
self.spotify.toggleplayback(); self.spotify.toggleplayback();
} }
pub fn stop(&mut self) { pub fn stop(&self) {
self.current_track = None; let mut current = self.current_track.write().unwrap();
*current = None;
self.spotify.stop(); self.spotify.stop();
} }
pub fn next(&mut self) { pub fn next(&self) {
if let Some(index) = self.next_index() { if let Some(index) = self.next_index() {
self.play(index); self.play(index);
} else { } else {
@@ -126,15 +138,11 @@ impl Queue {
} }
} }
pub fn previous(&mut self) { pub fn previous(&self) {
if let Some(index) = self.previous_index() { if let Some(index) = self.previous_index() {
self.play(index); self.play(index);
} else { } else {
self.spotify.stop(); self.spotify.stop();
} }
} }
pub fn iter(&self) -> Iter<Track> {
self.queue.iter()
}
} }

View File

@@ -1,7 +1,11 @@
use std::fmt; use std::fmt;
use std::sync::Arc;
use rspotify::spotify::model::track::FullTrack; use rspotify::spotify::model::track::FullTrack;
use queue::Queue;
use traits::ListItem;
#[derive(Clone, Deserialize, Serialize)] #[derive(Clone, Deserialize, Serialize)]
pub struct Track { pub struct Track {
pub id: String, 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 playlist;
pub mod queue; pub mod queue;
pub mod search; pub mod search;
pub mod splitbutton;
pub mod statusbar; 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::traits::Identifiable;
use cursive::views::*; use cursive::view::{ViewWrapper};
use cursive::Cursive; use cursive::views::{IdView, ScrollView};
use playlists::{Playlist, PlaylistEvent, Playlists}; use playlists::{Playlist, Playlists};
use queue::Queue; use queue::Queue;
use ui::splitbutton::SplitButton; use ui::listview::ListView;
pub struct PlaylistView { pub struct PlaylistView {
pub view: Option<BoxView<ScrollView<IdView<LinearLayout>>>>, list: ScrollView<IdView<ListView<Playlist>>>
queue: Arc<Mutex<Queue>>,
playlists: Playlists,
} }
impl PlaylistView { impl PlaylistView {
pub fn new(playlists: &Playlists, queue: Arc<Mutex<Queue>>) -> PlaylistView { pub fn new(playlists: &Playlists, queue: Arc<Queue>) -> PlaylistView {
let playlists_view = LinearLayout::new(Orientation::Vertical).with_id("playlists"); let list = ListView::new(playlists.store.clone(), queue).with_id("list");
let scrollable = ScrollView::new(playlists_view).full_screen(); let scrollable = ScrollView::new(list);
PlaylistView { PlaylistView {
view: Some(scrollable), list: 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);
}
}
} }
} }
} }
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::traits::Identifiable;
use cursive::views::*; use cursive::view::{ViewWrapper};
use cursive::Cursive; use cursive::views::{IdView, ScrollView};
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex;
use queue::Queue; use queue::Queue;
use track::Track; use track::Track;
use ui::splitbutton::SplitButton; use ui::listview::ListView;
use ui::trackbutton::TrackButton;
pub struct QueueView { pub struct QueueView {
pub view: Option<BoxView<ScrollView<IdView<LinearLayout>>>>, list: ScrollView<IdView<ListView<Track>>>
queue: Arc<Mutex<Queue>>,
} }
impl QueueView { impl QueueView {
pub fn new(queue: Arc<Mutex<Queue>>) -> QueueView { pub fn new(queue: Arc<Queue>) -> QueueView {
let queuelist = LinearLayout::new(Orientation::Vertical).with_id("queue_list"); let list = ListView::new(queue.queue.clone(), queue.clone()).with_id("queue_list");
let scrollable = ScrollView::new(queuelist).full_screen(); let scrollable = ScrollView::new(list);
QueueView { QueueView {
view: Some(scrollable), list: 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);
}
} }
} }
} }
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::direction::Orientation;
use cursive::event::Key; use cursive::event::{AnyCb, Event, EventResult, Key};
use cursive::traits::Boxable; use cursive::traits::{Boxable, Identifiable, Finder, View};
use cursive::traits::Identifiable; use cursive::view::{Selector, ViewWrapper};
use cursive::views::*; use cursive::views::{EditView, IdView, ScrollView, ViewRef};
use cursive::Cursive; use cursive::{Cursive, Printer, Vec2};
use std::sync::Arc; use std::cell::RefCell;
use std::sync::Mutex; use std::sync::{Arc, Mutex, RwLock};
use queue::Queue; use queue::Queue;
use spotify::Spotify; use spotify::Spotify;
use track::Track; use track::Track;
use ui::trackbutton::TrackButton; use ui::listview::ListView;
pub struct SearchView { pub struct SearchView {
pub view: LinearLayout, results: Arc<RwLock<Vec<Track>>>,
edit: IdView<EditView>,
list: ScrollView<IdView<ListView<Track>>>,
edit_focused: bool,
} }
impl SearchView { impl SearchView {
fn search_handler( pub fn new(spotify: Arc<Spotify>, queue: Arc<Queue>) -> SearchView {
s: &mut Cursive, let results = Arc::new(RwLock::new(Vec::new()));
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);
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() let searchfield = EditView::new()
.on_submit(move |s, input| { .on_submit(move |s, input| {
if input.len() > 0 { 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") .with_id("search_edit");
.full_width() let list = ListView::new(results.clone(), queue).with_id("list");
.fixed_height(1); let scrollable = ScrollView::new(list);
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);
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::align::HAlign;
use cursive::theme::ColorStyle; use cursive::theme::ColorStyle;
@@ -11,12 +11,12 @@ use queue::Queue;
use spotify::{PlayerEvent, Spotify}; use spotify::{PlayerEvent, Spotify};
pub struct StatusBar { pub struct StatusBar {
queue: Arc<Mutex<Queue>>, queue: Arc<Queue>,
spotify: Arc<Spotify>, spotify: Arc<Spotify>,
} }
impl StatusBar { impl StatusBar {
pub fn new(queue: Arc<Mutex<Queue>>, spotify: Arc<Spotify>) -> StatusBar { pub fn new(queue: Arc<Queue>, spotify: Arc<Spotify>) -> StatusBar {
StatusBar { StatusBar {
queue: queue, queue: queue,
spotify: spotify, spotify: spotify,
@@ -55,12 +55,7 @@ impl View for StatusBar {
printer.print((0, 1), &state_icon); printer.print((0, 1), &state_icon);
}); });
if let Some(ref t) = self if let Some(ref t) = self.queue.get_current() {
.queue
.lock()
.expect("could not lock queue")
.get_current()
{
let elapsed = self.spotify.get_current_progress(); let elapsed = self.spotify.get_current_progress();
let formatted_elapsed = format!( let formatted_elapsed = format!(
"{:02}:{:02}", "{: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
}
}