Refactor command handling
This commit is contained in:
341
src/commands.rs
341
src/commands.rs
@@ -2,39 +2,45 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use cursive::event::{Event, Key};
|
||||
use cursive::views::ViewRef;
|
||||
use cursive::Cursive;
|
||||
|
||||
use playlists::{Playlist, Playlists};
|
||||
use playlists::Playlists;
|
||||
use queue::{Queue, RepeatSetting};
|
||||
use spotify::Spotify;
|
||||
use track::Track;
|
||||
use ui;
|
||||
use traits::ViewExt;
|
||||
use ui::layout::Layout;
|
||||
use ui::listview::ListView;
|
||||
use ui::search::SearchView;
|
||||
|
||||
type CommandResult = Result<Option<String>, String>;
|
||||
type CommandCb = dyn Fn(&mut Cursive, Vec<String>) -> CommandResult;
|
||||
type CommandCb = dyn Fn(&mut Cursive, &[String]) -> Result<Option<String>, String>;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum CommandResult {
|
||||
Consumed(Option<String>),
|
||||
Ignored,
|
||||
}
|
||||
|
||||
pub struct CommandManager {
|
||||
commands: HashMap<String, Box<CommandCb>>,
|
||||
callbacks: HashMap<String, Option<Box<CommandCb>>>,
|
||||
aliases: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl CommandManager {
|
||||
pub fn new() -> CommandManager {
|
||||
CommandManager {
|
||||
commands: HashMap::new(),
|
||||
callbacks: HashMap::new(),
|
||||
aliases: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<S: Into<String>>(&mut self, name: S, aliases: Vec<S>, cb: Box<CommandCb>) {
|
||||
pub fn register_command<S: Into<String>>(&mut self, name: S, cb: Option<Box<CommandCb>>) {
|
||||
self.callbacks.insert(name.into(), cb);
|
||||
}
|
||||
|
||||
pub fn register_aliases<S: Into<String>>(&mut self, name: S, aliases: Vec<S>) {
|
||||
let name = name.into();
|
||||
for a in aliases {
|
||||
self.aliases.insert(a.into(), name.clone());
|
||||
}
|
||||
self.commands.insert(name, cb);
|
||||
}
|
||||
|
||||
pub fn register_all(
|
||||
@@ -43,272 +49,100 @@ impl CommandManager {
|
||||
queue: Arc<Queue>,
|
||||
playlists: Arc<Playlists>,
|
||||
) {
|
||||
self.register(
|
||||
self.register_aliases("quit", vec!["q", "x"]);
|
||||
self.register_aliases("playpause", vec!["pause", "toggleplay", "toggleplayback"]);
|
||||
self.register_aliases("repeat", vec!["loop"]);
|
||||
|
||||
self.register_command("search", None);
|
||||
self.register_command("move", None);
|
||||
self.register_command("play", None);
|
||||
self.register_command("queue", None);
|
||||
self.register_command("delete", None);
|
||||
|
||||
self.register_command(
|
||||
"quit",
|
||||
vec!["q", "x"],
|
||||
Box::new(move |s, _args| {
|
||||
Some(Box::new(move |s, _args| {
|
||||
s.quit();
|
||||
Ok(None)
|
||||
}),
|
||||
})),
|
||||
);
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
self.register_command(
|
||||
"stop",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
Some(Box::new(move |_s, _args| {
|
||||
queue.stop();
|
||||
Ok(None)
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
self.register_command(
|
||||
"previous",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
Some(Box::new(move |_s, _args| {
|
||||
queue.previous();
|
||||
Ok(None)
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
self.register_command(
|
||||
"next",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
Some(Box::new(move |_s, _args| {
|
||||
queue.next(true);
|
||||
Ok(None)
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
self.register_command(
|
||||
"clear",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, _args| {
|
||||
Some(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.is_empty() {
|
||||
v.run_search(args.join(" "), spotify.clone());
|
||||
}
|
||||
});
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
self.register(
|
||||
let playlists = playlists.clone();
|
||||
self.register_command(
|
||||
"playlists",
|
||||
vec!["lists"],
|
||||
Box::new(move |s, args| {
|
||||
Some(Box::new(move |_s, args| {
|
||||
if let Some(arg) = args.get(0) {
|
||||
if arg == "update" {
|
||||
playlists.fetch_playlists();
|
||||
playlists.save_cache();
|
||||
}
|
||||
} else {
|
||||
s.call_on_id("main", |v: &mut Layout| {
|
||||
v.set_view("playlists");
|
||||
});
|
||||
}
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
self.register(
|
||||
"move",
|
||||
Vec::new(),
|
||||
Box::new(move |s, args| {
|
||||
if args.is_empty() {
|
||||
return Err("Missing direction (up, down, left, right)".to_string());
|
||||
}
|
||||
|
||||
let dir = &args[0];
|
||||
|
||||
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(ui::queue::LIST_ID, |v: &mut ListView<Track>| {
|
||||
v.move_focus(dir * amount);
|
||||
});
|
||||
s.call_on_id(ui::search::LIST_ID, |v: &mut ListView<Track>| {
|
||||
v.move_focus(dir * amount);
|
||||
});
|
||||
s.call_on_id(ui::playlists::LIST_ID, |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(ui::search::LIST_ID, |v: &mut ListView<Track>| {
|
||||
v.with_selected(Box::new(move |t| {
|
||||
queue.append(t);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
s.call_on_id(ui::playlists::LIST_ID, |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(ui::queue::LIST_ID, |v: &mut ListView<Track>| {
|
||||
queue.play(v.get_selected_index(), true);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
s.call_on_id(ui::search::LIST_ID, |v: &mut ListView<Track>| {
|
||||
v.with_selected(Box::new(move |t| {
|
||||
let index = queue.append_next(vec![t]);
|
||||
queue.play(index, true);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
s.call_on_id(ui::playlists::LIST_ID, |v: &mut ListView<Playlist>| {
|
||||
v.with_selected(Box::new(move |pl| {
|
||||
let index = queue.append_next(pl.tracks.iter().collect());
|
||||
queue.play(index, true);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
self.register_command(
|
||||
"playpause",
|
||||
Some(Box::new(move |_s, _args| {
|
||||
queue.toggleplayback();
|
||||
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(ui::queue::LIST_ID, |v: &mut ListView<Track>| {
|
||||
queue.remove(v.get_selected_index());
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
if let Some(Some(dialog)) = s
|
||||
.call_on_id("playlists", |v: &mut ui::playlists::PlaylistView| {
|
||||
v.delete_dialog()
|
||||
})
|
||||
{
|
||||
s.add_layer(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
self.register_command(
|
||||
"shuffle",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, args| {
|
||||
Some(Box::new(move |_s, args| {
|
||||
if let Some(arg) = args.get(0) {
|
||||
queue.set_shuffle(match arg.as_ref() {
|
||||
"on" => true,
|
||||
@@ -322,16 +156,15 @@ impl CommandManager {
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let queue = queue.clone();
|
||||
self.register(
|
||||
self.register_command(
|
||||
"repeat",
|
||||
vec!["loop"],
|
||||
Box::new(move |_s, args| {
|
||||
Some(Box::new(move |_s, args| {
|
||||
if let Some(arg) = args.get(0) {
|
||||
queue.set_repeat(match arg.as_ref() {
|
||||
"list" | "playlist" | "queue" => RepeatSetting::RepeatPlaylist,
|
||||
@@ -350,16 +183,15 @@ impl CommandManager {
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let spotify = spotify.clone();
|
||||
self.register(
|
||||
self.register_command(
|
||||
"seek",
|
||||
Vec::new(),
|
||||
Box::new(move |_s, args| {
|
||||
Some(Box::new(move |_s, args| {
|
||||
if let Some(arg) = args.get(0) {
|
||||
match arg.chars().next().unwrap() {
|
||||
'+' | '-' => {
|
||||
@@ -372,7 +204,7 @@ impl CommandManager {
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -385,21 +217,36 @@ impl CommandManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_callbacks(&self, s: &mut Cursive, cmd: &String, args: &[String]) -> Result<Option<String>, String> {
|
||||
let local = {
|
||||
let mut main: ViewRef<Layout> = s.find_id("main").unwrap();
|
||||
main.on_command(s, cmd, args)?
|
||||
};
|
||||
|
||||
if let CommandResult::Consumed(output) = local {
|
||||
Ok(output)
|
||||
} else if let Some(callback) = self.callbacks.get(cmd) {
|
||||
callback.as_ref()
|
||||
.map(|cb| cb(s, args))
|
||||
.unwrap_or(Ok(None))
|
||||
} else {
|
||||
Err("Unknown command.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(&self, s: &mut Cursive, cmd: String) {
|
||||
let components: Vec<String> = cmd.trim().split(' ').map(|s| s.to_string()).collect();
|
||||
|
||||
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())
|
||||
};
|
||||
let cmd = self.handle_aliases(&components[0]);
|
||||
let args = components[1..].to_vec();
|
||||
|
||||
// TODO: handle non-error output as well
|
||||
if let Err(e) = result {
|
||||
s.call_on_id("main", |v: &mut Layout| {
|
||||
v.set_error(e);
|
||||
});
|
||||
}
|
||||
let result = self.handle_callbacks(s, &cmd, &args);
|
||||
|
||||
s.call_on_id("main", |v: &mut Layout| {
|
||||
v.set_result(result);
|
||||
});
|
||||
|
||||
s.on_event(Event::Refresh);
|
||||
}
|
||||
|
||||
pub fn register_keybinding<E: Into<cursive::event::Event>, S: Into<String>>(
|
||||
@@ -431,24 +278,24 @@ impl CommandManager {
|
||||
let mut kb = HashMap::new();
|
||||
|
||||
kb.insert("q".into(), "quit".into());
|
||||
kb.insert("P".into(), "toggle".into());
|
||||
kb.insert("P".into(), "toggleplay".into());
|
||||
kb.insert("R".into(), "playlists update".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(" ".into(), "queue".into());
|
||||
kb.insert("Enter".into(), "play".into());
|
||||
kb.insert("d".into(), "delete".into());
|
||||
kb.insert("/".into(), "focus search".into());
|
||||
kb.insert(".".into(), "seek +500".into());
|
||||
kb.insert(",".into(), "seek -500".into());
|
||||
kb.insert("r".into(), "repeat".into());
|
||||
kb.insert("z".into(), "shuffle".into());
|
||||
|
||||
kb.insert("F1".into(), "queue".into());
|
||||
kb.insert("F2".into(), "search".into());
|
||||
kb.insert("F3".into(), "playlists".into());
|
||||
kb.insert("F1".into(), "focus queue".into());
|
||||
kb.insert("F2".into(), "focus search".into());
|
||||
kb.insert("F3".into(), "focus playlists".into());
|
||||
|
||||
kb.insert("Up".into(), "move up".into());
|
||||
kb.insert("Down".into(), "move down".into());
|
||||
|
||||
16
src/main.rs
16
src/main.rs
@@ -133,8 +133,6 @@ fn main() {
|
||||
#[cfg(feature = "mpris")]
|
||||
let mpris_manager = Arc::new(mpris::MprisManager::new(spotify.clone(), queue.clone()));
|
||||
|
||||
let search = ui::search::SearchView::new(spotify.clone(), queue.clone());
|
||||
|
||||
let playlists = Arc::new(Playlists::new(&event_manager, &spotify));
|
||||
|
||||
{
|
||||
@@ -152,6 +150,14 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
let mut cmd_manager = CommandManager::new();
|
||||
cmd_manager.register_all(spotify.clone(), queue.clone(), playlists.clone());
|
||||
|
||||
let cmd_manager = Arc::new(cmd_manager);
|
||||
CommandManager::register_keybindings(cmd_manager.clone(), &mut cursive, cfg.keybindings.clone());
|
||||
|
||||
let search = ui::search::SearchView::new(spotify.clone(), queue.clone());
|
||||
|
||||
let playlistsview = ui::playlists::PlaylistView::new(&playlists, queue.clone());
|
||||
|
||||
let queueview = ui::queue::QueueView::new(queue.clone(), playlists.clone());
|
||||
@@ -192,12 +198,6 @@ fn main() {
|
||||
|
||||
cursive.add_fullscreen_layer(layout.with_id("main"));
|
||||
|
||||
let mut cmd_manager = CommandManager::new();
|
||||
cmd_manager.register_all(spotify.clone(), queue.clone(), playlists.clone());
|
||||
|
||||
let cmd_manager = Arc::new(cmd_manager);
|
||||
CommandManager::register_keybindings(cmd_manager.clone(), &mut cursive, cfg.keybindings);
|
||||
|
||||
// cursive event loop
|
||||
while cursive.is_running() {
|
||||
cursive.step();
|
||||
|
||||
@@ -48,6 +48,17 @@ impl ListItem for Playlist {
|
||||
fn display_right(&self) -> String {
|
||||
format!("{} tracks", self.tracks.len())
|
||||
}
|
||||
|
||||
fn play(&self, queue: Arc<Queue>) {
|
||||
let index = queue.append_next(self.tracks.iter().collect());
|
||||
queue.play(index, true);
|
||||
}
|
||||
|
||||
fn queue(&self, queue: Arc<Queue>) {
|
||||
for track in self.tracks.iter() {
|
||||
queue.append(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Playlists {
|
||||
|
||||
@@ -91,4 +91,13 @@ impl ListItem for Track {
|
||||
fn display_right(&self) -> String {
|
||||
self.duration_str()
|
||||
}
|
||||
|
||||
fn play(&self, queue: Arc<Queue>) {
|
||||
let index = queue.append_next(vec![self]);
|
||||
queue.play(index, true);
|
||||
}
|
||||
|
||||
fn queue(&self, queue: Arc<Queue>) {
|
||||
queue.append(self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,48 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use cursive::view::{View, ViewWrapper};
|
||||
use cursive::views::IdView;
|
||||
use cursive::Cursive;
|
||||
|
||||
use commands::CommandResult;
|
||||
use queue::Queue;
|
||||
|
||||
pub trait ListItem {
|
||||
fn is_playing(&self, queue: Arc<Queue>) -> bool;
|
||||
fn display_left(&self) -> String;
|
||||
fn display_right(&self) -> String;
|
||||
fn play(&self, queue: Arc<Queue>);
|
||||
fn queue(&self, queue: Arc<Queue>);
|
||||
}
|
||||
|
||||
pub trait ViewExt: View {
|
||||
fn on_command(&mut self,
|
||||
_s: &mut Cursive,
|
||||
_cmd: &String,
|
||||
_args: &[String]
|
||||
) -> Result<CommandResult, String> {
|
||||
Ok(CommandResult::Ignored)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: ViewExt> ViewExt for IdView<V> {
|
||||
fn on_command(&mut self,
|
||||
s: &mut Cursive,
|
||||
cmd: &String,
|
||||
args: &[String]
|
||||
) -> Result<CommandResult, String> {
|
||||
self.with_view_mut(move |v| {
|
||||
v.on_command(s, cmd, args)
|
||||
}).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoBoxedViewExt {
|
||||
fn as_boxed_view_ext(self) -> Box<dyn ViewExt>;
|
||||
}
|
||||
|
||||
impl<V: ViewExt> IntoBoxedViewExt for V {
|
||||
fn as_boxed_view_ext(self) -> Box<dyn ViewExt> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,16 @@ use cursive::traits::View;
|
||||
use cursive::vec::Vec2;
|
||||
use cursive::view::{IntoBoxedView, Selector};
|
||||
use cursive::views::EditView;
|
||||
use cursive::Printer;
|
||||
use cursive::{Cursive, Printer};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use events;
|
||||
use traits::{ViewExt, IntoBoxedViewExt};
|
||||
use commands::CommandResult;
|
||||
|
||||
struct Screen {
|
||||
title: String,
|
||||
view: Box<dyn View>,
|
||||
view: Box<dyn ViewExt>,
|
||||
}
|
||||
|
||||
pub struct Layout {
|
||||
@@ -26,8 +28,8 @@ pub struct Layout {
|
||||
focus: Option<String>,
|
||||
pub cmdline: EditView,
|
||||
cmdline_focus: bool,
|
||||
error: Option<String>,
|
||||
error_time: Option<SystemTime>,
|
||||
result: Result<Option<String>, String>,
|
||||
result_time: Option<SystemTime>,
|
||||
screenchange: bool,
|
||||
last_size: Vec2,
|
||||
ev: events::EventManager,
|
||||
@@ -48,8 +50,8 @@ impl Layout {
|
||||
focus: None,
|
||||
cmdline: EditView::new().filler(" ").style(style),
|
||||
cmdline_focus: false,
|
||||
error: None,
|
||||
error_time: None,
|
||||
result: Ok(None),
|
||||
result_time: None,
|
||||
screenchange: true,
|
||||
last_size: Vec2::new(0, 0),
|
||||
ev: ev.clone(),
|
||||
@@ -64,18 +66,18 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_view<S: Into<String>, T: IntoBoxedView>(&mut self, id: S, view: T, title: &str) {
|
||||
pub fn add_view<S: Into<String>, T: IntoBoxedViewExt>(&mut self, id: S, view: T, title: &str) {
|
||||
let s = id.into();
|
||||
let screen = Screen {
|
||||
title: title.to_string(),
|
||||
view: view.as_boxed_view(),
|
||||
view: view.as_boxed_view_ext(),
|
||||
};
|
||||
self.views.insert(s.clone(), screen);
|
||||
self.title = title.to_owned();
|
||||
self.focus = Some(s);
|
||||
}
|
||||
|
||||
pub fn view<S: Into<String>, T: IntoBoxedView>(mut self, id: S, view: T, title: &str) -> Self {
|
||||
pub fn view<S: Into<String>, T: IntoBoxedViewExt>(mut self, id: S, view: T, title: &str) -> Self {
|
||||
(&mut self).add_view(id, view, title);
|
||||
self
|
||||
}
|
||||
@@ -92,35 +94,35 @@ impl Layout {
|
||||
self.ev.trigger();
|
||||
}
|
||||
|
||||
pub fn set_error<S: Into<String>>(&mut self, error: S) {
|
||||
self.error = Some(error.into());
|
||||
self.error_time = Some(SystemTime::now());
|
||||
pub fn set_result(&mut self, result: Result<Option<String>, String>) {
|
||||
self.result = result;
|
||||
self.result_time = Some(SystemTime::now());
|
||||
}
|
||||
|
||||
pub fn clear_cmdline(&mut self) {
|
||||
self.cmdline.set_content("");
|
||||
self.cmdline_focus = false;
|
||||
self.error = None;
|
||||
self.error_time = None;
|
||||
self.result = Ok(None);
|
||||
self.result_time = None;
|
||||
}
|
||||
|
||||
fn get_error(&self) -> Option<String> {
|
||||
if let Some(t) = self.error_time {
|
||||
fn get_result(&self) -> Result<Option<String>, String> {
|
||||
if let Some(t) = self.result_time {
|
||||
if t.elapsed().unwrap() > Duration::from_secs(5) {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
self.error.clone()
|
||||
self.result.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl View for Layout {
|
||||
fn draw(&self, printer: &Printer<'_, '_>) {
|
||||
let error = self.get_error();
|
||||
let result = self.get_result();
|
||||
|
||||
let cmdline_visible = self.cmdline.get_content().len() > 0;
|
||||
let mut cmdline_height = if cmdline_visible { 1 } else { 0 };
|
||||
if error.is_some() {
|
||||
if result.as_ref().map(|o| o.is_some()).unwrap_or(true) {
|
||||
cmdline_height += 1;
|
||||
}
|
||||
|
||||
@@ -143,17 +145,18 @@ impl View for Layout {
|
||||
self.statusbar
|
||||
.draw(&printer.offset((0, printer.size.y - 2 - cmdline_height)));
|
||||
|
||||
if let Some(e) = error {
|
||||
if let Ok(Some(r)) = result {
|
||||
printer.print_hline((0, printer.size.y - cmdline_height), printer.size.x, " ");
|
||||
printer.print((0, printer.size.y - cmdline_height), &r);
|
||||
} else if let Err(e) = result {
|
||||
let style = ColorStyle::new(
|
||||
ColorType::Color(*self.theme.palette.custom("error").unwrap()),
|
||||
ColorType::Color(*self.theme.palette.custom("error_bg").unwrap()),
|
||||
);
|
||||
|
||||
printer.with_color(style, |printer| {
|
||||
printer.print_hline((0, printer.size.y - cmdline_height), printer.size.x, " ");
|
||||
printer.print(
|
||||
(0, printer.size.y - cmdline_height),
|
||||
&format!("ERROR: {}", e),
|
||||
);
|
||||
printer.print((0, printer.size.y - cmdline_height), &format!("ERROR: {}", e));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -169,11 +172,11 @@ impl View for Layout {
|
||||
|
||||
fn on_event(&mut self, event: Event) -> EventResult {
|
||||
if let Event::Mouse { position, .. } = event {
|
||||
let error = self.get_error();
|
||||
let result = self.get_result();
|
||||
|
||||
let cmdline_visible = self.cmdline.get_content().len() > 0;
|
||||
let mut cmdline_height = if cmdline_visible { 1 } else { 0 };
|
||||
if error.is_some() {
|
||||
if result.as_ref().map(|o| o.is_some()).unwrap_or(true) {
|
||||
cmdline_height += 1;
|
||||
}
|
||||
|
||||
@@ -243,3 +246,26 @@ impl View for Layout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewExt for Layout {
|
||||
fn on_command(&mut self,
|
||||
s: &mut Cursive,
|
||||
cmd: &String,
|
||||
args: &[String]
|
||||
) -> Result<CommandResult, String> {
|
||||
if cmd == "focus" {
|
||||
if let Some(view) = args.get(0) {
|
||||
if self.views.keys().any(|k| k == view) {
|
||||
self.set_view(view.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CommandResult::Consumed(None))
|
||||
} else if let Some(ref id) = self.focus {
|
||||
let screen = self.views.get_mut(id).unwrap();
|
||||
screen.view.on_command(s, cmd, args)
|
||||
} else {
|
||||
Ok(CommandResult::Ignored)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
use std::cmp::{max, min};
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use cursive::align::HAlign;
|
||||
use cursive::event::{Event, EventResult, MouseButton, MouseEvent};
|
||||
use cursive::theme::{ColorStyle, ColorType, PaletteColor};
|
||||
use cursive::traits::View;
|
||||
use cursive::view::ScrollBase;
|
||||
use cursive::{Printer, Rect, Vec2};
|
||||
use cursive::{Cursive, Printer, Rect, Vec2};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use queue::Queue;
|
||||
use traits::ListItem;
|
||||
use traits::{ListItem, ViewExt};
|
||||
use commands::CommandResult;
|
||||
|
||||
pub struct ListView<I: 'static + ListItem> {
|
||||
content: Arc<RwLock<Vec<I>>>,
|
||||
last_content_length: usize,
|
||||
last_content_len: usize,
|
||||
selected: usize,
|
||||
last_size: Vec2,
|
||||
scrollbar: ScrollBase,
|
||||
@@ -25,7 +26,7 @@ impl<I: ListItem> ListView<I> {
|
||||
pub fn new(content: Arc<RwLock<Vec<I>>>, queue: Arc<Queue>) -> Self {
|
||||
Self {
|
||||
content,
|
||||
last_content_length: 0,
|
||||
last_content_len: 0,
|
||||
selected: 0,
|
||||
last_size: Vec2::new(0, 0),
|
||||
scrollbar: ScrollBase::new(),
|
||||
@@ -33,13 +34,6 @@ impl<I: ListItem> ListView<I> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_selected(&self, cb: Box<Fn(&I) -> ()>) {
|
||||
match self.content.read().unwrap().get(self.selected) {
|
||||
Some(x) => cb(x),
|
||||
None => error!("listview: invalid item index: {})", self.selected),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_index(&self) -> usize {
|
||||
self.selected
|
||||
}
|
||||
@@ -54,12 +48,6 @@ impl<I: ListItem> ListView<I> {
|
||||
let new = self.selected as i32 + delta;
|
||||
self.move_focus_to(max(new, 0) as usize);
|
||||
}
|
||||
|
||||
pub fn content(&self) -> RwLockReadGuard<Vec<I>> {
|
||||
self.content
|
||||
.read()
|
||||
.expect("could not readlock listview content")
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: ListItem> View for ListView<I> {
|
||||
@@ -116,13 +104,13 @@ impl<I: ListItem> View for ListView<I> {
|
||||
}
|
||||
|
||||
fn layout(&mut self, size: Vec2) {
|
||||
self.last_content_length = self.content.read().unwrap().len();
|
||||
self.last_content_len = self.content.read().unwrap().len();
|
||||
self.last_size = size;
|
||||
self.scrollbar.set_heights(size.y, self.last_content_length);
|
||||
self.scrollbar.set_heights(size.y, self.last_content_len);
|
||||
}
|
||||
|
||||
fn needs_relayout(&self) -> bool {
|
||||
self.content.read().unwrap().len() != self.last_content_length
|
||||
self.content.read().unwrap().len() != self.last_content_len
|
||||
}
|
||||
|
||||
fn on_event(&mut self, e: Event) -> EventResult {
|
||||
@@ -184,3 +172,53 @@ impl<I: ListItem> View for ListView<I> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: ListItem> ViewExt for ListView<I> {
|
||||
fn on_command(&mut self,
|
||||
_s: &mut Cursive,
|
||||
cmd: &String,
|
||||
args: &[String]
|
||||
) -> Result<CommandResult, String> {
|
||||
if cmd == "play" {
|
||||
let content = self.content.read().unwrap();
|
||||
if let Some(item) = content.get(self.selected) {
|
||||
item.play(self.queue.clone());
|
||||
}
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
|
||||
if cmd == "queue" {
|
||||
let content = self.content.read().unwrap();
|
||||
if let Some(item) = content.get(self.selected) {
|
||||
item.queue(self.queue.clone());
|
||||
}
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
|
||||
if cmd != "move" {
|
||||
return Ok(CommandResult::Ignored);
|
||||
}
|
||||
|
||||
if let Some(dir) = args.get(0) {
|
||||
let amount: i32 = args
|
||||
.get(1)
|
||||
.unwrap_or(&"1".to_string())
|
||||
.parse()
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
|
||||
let len = self.content.read().unwrap().len();
|
||||
|
||||
if dir == "up" && self.selected > 0 {
|
||||
self.move_focus(amount * -1);
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
|
||||
if dir == "down" && self.selected < len - 1 {
|
||||
self.move_focus(amount);
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CommandResult::Ignored)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ use cursive::view::ViewWrapper;
|
||||
use cursive::views::{Dialog, IdView};
|
||||
use cursive::Cursive;
|
||||
|
||||
use commands::CommandResult;
|
||||
use playlists::{Playlist, Playlists};
|
||||
use queue::Queue;
|
||||
use traits::ViewExt;
|
||||
use ui::listview::ListView;
|
||||
use ui::modal::Modal;
|
||||
|
||||
@@ -52,3 +54,21 @@ impl PlaylistView {
|
||||
impl ViewWrapper for PlaylistView {
|
||||
wrap_impl!(self.list: IdView<ListView<Playlist>>);
|
||||
}
|
||||
|
||||
impl ViewExt for PlaylistView {
|
||||
fn on_command(&mut self,
|
||||
s: &mut Cursive,
|
||||
cmd: &String,
|
||||
args: &[String]
|
||||
) -> Result<CommandResult, String> {
|
||||
if cmd == "delete" {
|
||||
if let Some(dialog) = self.delete_dialog() {
|
||||
s.add_layer(dialog);
|
||||
}
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
|
||||
self.list.on_command(s, cmd, args)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
use cursive::event::{Callback, Event, EventResult};
|
||||
use cursive::traits::{Boxable, Identifiable, View};
|
||||
use cursive::view::ViewWrapper;
|
||||
use cursive::views::{Dialog, EditView, IdView, ScrollView, SelectView};
|
||||
use cursive::views::{Dialog, EditView, ScrollView, SelectView};
|
||||
use cursive::Cursive;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use commands::CommandResult;
|
||||
use playlists::Playlists;
|
||||
use queue::Queue;
|
||||
use track::Track;
|
||||
use traits::ViewExt;
|
||||
use ui::listview::ListView;
|
||||
use ui::modal::Modal;
|
||||
|
||||
pub struct QueueView {
|
||||
list: IdView<ListView<Track>>,
|
||||
list: ListView<Track>,
|
||||
playlists: Arc<Playlists>,
|
||||
queue: Arc<Queue>,
|
||||
}
|
||||
|
||||
pub const LIST_ID: &str = "queue_list";
|
||||
impl QueueView {
|
||||
pub fn new(queue: Arc<Queue>, playlists: Arc<Playlists>) -> QueueView {
|
||||
let list = ListView::new(queue.queue.clone(), queue.clone()).with_id(LIST_ID);
|
||||
let list = ListView::new(queue.queue.clone(), queue.clone());
|
||||
|
||||
QueueView { list, playlists }
|
||||
QueueView { list, playlists, queue }
|
||||
}
|
||||
|
||||
fn save_dialog_cb(s: &mut Cursive, playlists: Arc<Playlists>, id: Option<String>) {
|
||||
let tracks = s
|
||||
.call_on_id(LIST_ID, |view: &mut ListView<_>| view.content().clone())
|
||||
.unwrap();
|
||||
fn save_dialog_cb(s: &mut Cursive, queue: Arc<Queue>, playlists: Arc<Playlists>, id: Option<String>) {
|
||||
let tracks = queue.queue.read().unwrap().clone();
|
||||
match id {
|
||||
Some(id) => {
|
||||
playlists.overwrite_playlist(&id, &tracks);
|
||||
@@ -53,7 +53,7 @@ impl QueueView {
|
||||
}
|
||||
}
|
||||
|
||||
fn save_dialog(playlists: Arc<Playlists>) -> Modal<Dialog> {
|
||||
fn save_dialog(queue: Arc<Queue>, playlists: Arc<Playlists>) -> Modal<Dialog> {
|
||||
let mut list_select: SelectView<Option<String>> = SelectView::new().autojump();
|
||||
list_select.add_item("[Create new]", None);
|
||||
|
||||
@@ -62,7 +62,7 @@ impl QueueView {
|
||||
}
|
||||
|
||||
list_select.set_on_submit(move |s, selected| {
|
||||
Self::save_dialog_cb(s, playlists.clone(), selected.clone())
|
||||
Self::save_dialog_cb(s, queue.clone(), playlists.clone(), selected.clone())
|
||||
});
|
||||
|
||||
let dialog = Dialog::new()
|
||||
@@ -75,15 +75,16 @@ impl QueueView {
|
||||
}
|
||||
|
||||
impl ViewWrapper for QueueView {
|
||||
wrap_impl!(self.list: IdView<ListView<Track>>);
|
||||
wrap_impl!(self.list: ListView<Track>);
|
||||
|
||||
fn wrap_on_event(&mut self, ch: Event) -> EventResult {
|
||||
match ch {
|
||||
Event::Char('s') => {
|
||||
debug!("save list");
|
||||
let queue = self.queue.clone();
|
||||
let playlists = self.playlists.clone();
|
||||
let cb = move |s: &mut Cursive| {
|
||||
let dialog = Self::save_dialog(playlists.clone());
|
||||
let dialog = Self::save_dialog(queue.clone(), playlists.clone());
|
||||
s.add_layer(dialog)
|
||||
};
|
||||
EventResult::Consumed(Some(Callback::from_fn(cb)))
|
||||
@@ -92,3 +93,29 @@ impl ViewWrapper for QueueView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewExt for QueueView {
|
||||
fn on_command(&mut self,
|
||||
s: &mut Cursive,
|
||||
cmd: &String,
|
||||
args: &[String]
|
||||
) -> Result<CommandResult, String> {
|
||||
if cmd == "play" {
|
||||
self.queue.play(self.list.get_selected_index(), true);
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
|
||||
if cmd == "queue" {
|
||||
return Ok(CommandResult::Ignored);
|
||||
}
|
||||
|
||||
if cmd == "delete" {
|
||||
self.queue.remove(self.list.get_selected_index());
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
|
||||
self.with_view_mut(move |v| {
|
||||
v.on_command(s, cmd, args)
|
||||
}).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ use cursive::{Cursive, Printer, Vec2};
|
||||
use std::cell::RefCell;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use commands::CommandResult;
|
||||
use queue::Queue;
|
||||
use spotify::Spotify;
|
||||
use track::Track;
|
||||
use traits::ViewExt;
|
||||
use ui::listview::ListView;
|
||||
|
||||
pub struct SearchView {
|
||||
@@ -19,6 +21,7 @@ pub struct SearchView {
|
||||
edit: IdView<EditView>,
|
||||
list: IdView<ListView<Track>>,
|
||||
edit_focused: bool,
|
||||
spotify: Arc<Spotify>,
|
||||
}
|
||||
|
||||
pub const LIST_ID: &str = "search_list";
|
||||
@@ -31,7 +34,7 @@ impl SearchView {
|
||||
.on_submit(move |s, input| {
|
||||
if !input.is_empty() {
|
||||
s.call_on_id("search", |v: &mut SearchView| {
|
||||
v.run_search(input, spotify.clone());
|
||||
v.run_search(input);
|
||||
v.focus_view(&Selector::Id(LIST_ID)).unwrap();
|
||||
});
|
||||
}
|
||||
@@ -44,10 +47,11 @@ impl SearchView {
|
||||
edit: searchfield,
|
||||
list,
|
||||
edit_focused: true,
|
||||
spotify
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_search<S: Into<String>>(&mut self, query: S, spotify: Arc<Spotify>) {
|
||||
pub fn run_search<S: Into<String>>(&mut self, query: S) {
|
||||
let query = query.into();
|
||||
let q = query.clone();
|
||||
self.edit
|
||||
@@ -55,7 +59,7 @@ impl SearchView {
|
||||
v.set_content(q);
|
||||
});
|
||||
|
||||
if let Some(results) = spotify.search(&query, 50, 0) {
|
||||
if let Some(results) = self.spotify.search(&query, 50, 0) {
|
||||
let tracks = results
|
||||
.tracks
|
||||
.items
|
||||
@@ -67,18 +71,6 @@ impl SearchView {
|
||||
self.edit_focused = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn list_index(&self) -> usize {
|
||||
self.list.with_view(|v| v.get_selected_index()).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn pass_event_focused(&mut self, event: Event) -> EventResult {
|
||||
if self.edit_focused {
|
||||
self.edit.on_event(event)
|
||||
} else {
|
||||
self.list.on_event(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View for SearchView {
|
||||
@@ -118,24 +110,46 @@ impl View for SearchView {
|
||||
}
|
||||
|
||||
fn on_event(&mut self, event: Event) -> EventResult {
|
||||
match event {
|
||||
Event::Key(Key::Tab) => {
|
||||
self.edit_focused = !self.edit_focused;
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Key(Key::Esc) if self.edit_focused => {
|
||||
self.edit_focused = false;
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Key(Key::Down) if self.edit_focused => {
|
||||
self.edit_focused = false;
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Key(Key::Up) if (!self.edit_focused && self.list_index() == 0) => {
|
||||
self.edit_focused = true;
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
_ => self.pass_event_focused(event),
|
||||
if self.edit_focused {
|
||||
self.edit.on_event(event)
|
||||
} else {
|
||||
self.list.on_event(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ViewExt for SearchView {
|
||||
fn on_command(&mut self,
|
||||
s: &mut Cursive,
|
||||
cmd: &String,
|
||||
args: &[String]
|
||||
) -> Result<CommandResult, String> {
|
||||
if cmd == "search" && !args.is_empty() {
|
||||
self.run_search(args.join(" "));
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
|
||||
let result = if !self.edit_focused {
|
||||
self.list.on_command(s, cmd, args)?
|
||||
} else {
|
||||
CommandResult::Ignored
|
||||
};
|
||||
|
||||
if result == CommandResult::Ignored && cmd == "move" {
|
||||
if let Some(dir) = args.get(0) {
|
||||
if dir == "up" && !self.edit_focused {
|
||||
self.edit_focused = true;
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
|
||||
if dir == "down" && self.edit_focused {
|
||||
self.edit_focused = false;
|
||||
return Ok(CommandResult::Consumed(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user