diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..9a8c3bd --- /dev/null +++ b/src/command.rs @@ -0,0 +1,219 @@ +use queue::RepeatSetting; +use std::collections::HashMap; +use std::iter::FromIterator; + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum PlaylistCommands { + Update, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum SeekInterval { + Forward, + Backwards, + Custom(usize), +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum TargetMode { + Current, + Selected, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum MoveMode { + Up, + Down, + Left, + Right, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum ShiftMode { + Up, + Down, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum GotoMode { + Album, + Artist, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum SeekDirection { + Relative(i32), + Absolute(u32), +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum Command { + Quit, + TogglePlay, + Playlists(PlaylistCommands), + Stop, + Previous, + Next, + Clear, + Queue, + Play, + Save, + SaveQueue, + Delete, + Focus(String), + Seek(SeekDirection), + Repeat(Option), + Shuffle(Option), + Share(TargetMode), + Back, + Open, + Goto(GotoMode), + Move(MoveMode, Option), + Shift(ShiftMode, Option), + Search(String), +} + +fn register_aliases(map: &mut HashMap<&str, &str>, cmd: &'static str, names: Vec<&'static str>) { + for a in names { + map.insert(a, cmd); + } +} + +lazy_static! { + static ref ALIASES: HashMap<&'static str, &'static str> = { + let mut m = HashMap::new(); + + register_aliases(&mut m, "quit", vec!["q", "x"]); + register_aliases( + &mut m, + "playpause", + vec!["pause", "toggleplay", "toggleplayback"], + ); + register_aliases(&mut m, "repeat", vec!["loop"]); + + m.insert("1", "foo"); + m.insert("2", "bar"); + m.insert("3", "baz"); + m + }; +} + +fn handle_aliases(input: &str) -> &str { + if let Some(cmd) = ALIASES.get(input) { + handle_aliases(cmd) + } else { + input + } +} + +pub fn parse(input: &str) -> Option { + let components: Vec<_> = input.trim().split(' ').collect(); + + let command = handle_aliases(&components[0]); + let args = components[1..].to_vec(); + + match command { + "quit" => Some(Command::Quit), + "playpause" => Some(Command::TogglePlay), + "stop" => Some(Command::Stop), + "previous" => Some(Command::Previous), + "next" => Some(Command::Next), + "clear" => Some(Command::Clear), + "queue" => Some(Command::Queue), + "play" => Some(Command::Play), + "delete" => Some(Command::Delete), + "back" => Some(Command::Back), + "open" => Some(Command::Open), + "search" => args.get(0).map(|query| Command::Search(query.to_string())), + "shift" => { + let amount = args.get(1).and_then(|amount| amount.parse().ok()); + + args.get(0) + .and_then(|direction| match *direction { + "up" => Some(ShiftMode::Up), + "down" => Some(ShiftMode::Down), + _ => None, + }) + .map(|mode| Command::Shift(mode, amount)) + } + "move" => { + let amount = args.get(1).and_then(|amount| amount.parse().ok()); + + args.get(0) + .and_then(|direction| match *direction { + "up" => Some(MoveMode::Up), + "down" => Some(MoveMode::Down), + "left" => Some(MoveMode::Left), + "right" => Some(MoveMode::Right), + _ => None, + }) + .map(|mode| Command::Move(mode, amount)) + } + "goto" => args + .get(0) + .and_then(|mode| match *mode { + "album" => Some(GotoMode::Album), + "artist" => Some(GotoMode::Artist), + _ => None, + }) + .map(Command::Goto), + "share" => args + .get(0) + .and_then(|target| match *target { + "selected" => Some(TargetMode::Selected), + "current" => Some(TargetMode::Current), + _ => None, + }) + .map(Command::Share), + "shuffle" => { + let shuffle = args.get(0).and_then(|mode| match *mode { + "on" => Some(true), + "off" => Some(false), + _ => None, + }); + + Some(Command::Shuffle(shuffle)) + } + "repeat" => { + let mode = args.get(0).and_then(|mode| match *mode { + "list" | "playlist" | "queue" => Some(RepeatSetting::RepeatPlaylist), + "track" | "once" => Some(RepeatSetting::RepeatTrack), + "none" | "off" => Some(RepeatSetting::None), + _ => None, + }); + + Some(Command::Repeat(mode)) + } + "seek" => args.get(0).and_then(|arg| match arg.chars().nth(0) { + Some(x) if x == '-' || x == '+' => String::from_iter(arg.chars().skip(1)) + .parse::() + .ok() + .map(|amount| { + Command::Seek(SeekDirection::Relative( + amount + * match x { + '-' => -1, + _ => 1, + }, + )) + }), + _ => String::from_iter(arg.chars()) + .parse() + .ok() + .map(|amount| Command::Seek(SeekDirection::Absolute(amount))), + }), + "focus" => args.get(0).map(|target| Command::Focus(target.to_string())), + "playlists" => args + .get(0) + .and_then(|action| match *action { + "update" => Some(PlaylistCommands::Update), + _ => None, + }) + .map(Command::Playlists), + "save" => args.get(0).map(|target| match *target { + "queue" => Command::SaveQueue, + _ => Command::Save, + }), + _ => None, + } +} diff --git a/src/commands.rs b/src/commands.rs index b0662b4..3483567 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -2,18 +2,18 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use command::{ + Command, GotoMode, MoveMode, PlaylistCommands, SeekDirection, ShiftMode, TargetMode, +}; use cursive::event::{Event, Key}; use cursive::views::ViewRef; use cursive::Cursive; - use library::Library; use queue::{Queue, RepeatSetting}; use spotify::Spotify; use traits::ViewExt; use ui::layout::Layout; -type CommandCb = dyn Fn(&mut Cursive, &[String]) -> Result, String>; - pub enum CommandResult { Consumed(Option), View(Box), @@ -21,22 +21,22 @@ pub enum CommandResult { } pub struct CommandManager { - callbacks: HashMap>>, aliases: HashMap, + spotify: Arc, + queue: Arc, + library: Arc, } impl CommandManager { - pub fn new() -> CommandManager { + pub fn new(spotify: Arc, queue: Arc, library: Arc) -> CommandManager { CommandManager { - callbacks: HashMap::new(), aliases: HashMap::new(), + spotify, + queue, + library, } } - pub fn register_command>(&mut self, name: S, cb: Option>) { - self.callbacks.insert(name.into(), cb); - } - pub fn register_aliases>(&mut self, name: S, aliases: Vec) { let name = name.into(); for a in aliases { @@ -44,198 +44,92 @@ impl CommandManager { } } - pub fn register_all( - &mut self, - spotify: Arc, - queue: Arc, - library: Arc, - ) { + pub fn register_all(&mut self) { 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("shift", None); - self.register_command("play", None); - self.register_command("queue", None); - self.register_command("save", None); - self.register_command("delete", None); - self.register_command("back", None); - self.register_command("open", None); - self.register_command("goto", None); - - self.register_command( - "quit", - Some(Box::new(move |s, _args| { - s.quit(); - Ok(None) - })), - ); - - { - let queue = queue.clone(); - self.register_command( - "stop", - Some(Box::new(move |_s, _args| { - queue.stop(); - Ok(None) - })), - ); - } - - { - let queue = queue.clone(); - let spotify = spotify.clone(); - self.register_command( - "previous", - Some(Box::new(move |_s, _args| { - if spotify.get_current_progress() < Duration::from_secs(5) { - queue.previous(); - } else { - spotify.seek(0); - } - Ok(None) - })), - ); - } - - { - let queue = queue.clone(); - self.register_command( - "next", - Some(Box::new(move |_s, _args| { - queue.next(true); - Ok(None) - })), - ); - } - - { - let queue = queue.clone(); - self.register_command( - "clear", - Some(Box::new(move |_s, _args| { - queue.clear(); - Ok(None) - })), - ); - } - - { - let library = library.clone(); - self.register_command( - "playlists", - Some(Box::new(move |_s, args| { - if let Some(arg) = args.get(0) { - if arg == "update" { - library.update_playlists(); - } - } - Ok(None) - })), - ); - } - - { - let queue = queue.clone(); - self.register_command( - "playpause", - Some(Box::new(move |_s, _args| { - queue.toggleplayback(); - Ok(None) - })), - ); - } - - { - let queue = queue.clone(); - self.register_command( - "shuffle", - Some(Box::new(move |_s, args| { - if let Some(arg) = args.get(0) { - queue.set_shuffle(match arg.as_ref() { - "on" => true, - "off" => false, - _ => { - return Err("Unknown shuffle setting.".to_string()); - } - }); - } else { - queue.set_shuffle(!queue.get_shuffle()); - } - - Ok(None) - })), - ); - } - - { - let queue = queue.clone(); - self.register_command( - "repeat", - 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, - "track" | "once" => RepeatSetting::RepeatTrack, - "none" | "off" => RepeatSetting::None, - _ => { - return Err("Unknown loop setting.".to_string()); - } - }); - } else { - queue.set_repeat(match queue.get_repeat() { - RepeatSetting::None => RepeatSetting::RepeatPlaylist, - RepeatSetting::RepeatPlaylist => RepeatSetting::RepeatTrack, - RepeatSetting::RepeatTrack => RepeatSetting::None, - }); - } - - Ok(None) - })), - ); - } - - { - let spotify = spotify.clone(); - self.register_command( - "seek", - Some(Box::new(move |_s, args| { - if let Some(arg) = args.get(0) { - match arg.chars().next().unwrap() { - '+' | '-' => { - spotify.seek_relative(arg.parse::().unwrap_or(0)); - } - _ => { - spotify.seek(arg.parse::().unwrap_or(0)); - } - } - } - - Ok(None) - })), - ); - } } - fn handle_aliases(&self, name: &str) -> String { - if let Some(s) = self.aliases.get(name) { - self.handle_aliases(s) - } else { - name.to_string() - } - } - - fn handle_callbacks( + fn handle_default_commands( &self, s: &mut Cursive, - cmd: &str, - args: &[String], + cmd: &Command, ) -> Result, String> { + match cmd { + Command::Quit => { + s.quit(); + Ok(None) + } + Command::Stop => { + self.queue.stop(); + Ok(None) + } + Command::Previous => { + if self.spotify.get_current_progress() < Duration::from_secs(5) { + self.queue.previous(); + } else { + self.spotify.seek(0); + } + Ok(None) + } + Command::Next => { + self.queue.next(true); + Ok(None) + } + Command::Clear => { + self.queue.clear(); + Ok(None) + } + Command::Playlists(mode) => { + match mode { + PlaylistCommands::Update => self.library.update_playlists(), + } + Ok(None) + } + Command::TogglePlay => { + self.queue.toggleplayback(); + Ok(None) + } + Command::Shuffle(mode) => { + let mode = mode.unwrap_or_else(|| !self.queue.get_shuffle()); + self.queue.set_shuffle(mode); + Ok(None) + } + Command::Repeat(mode) => { + let mode = mode.unwrap_or_else(|| match self.queue.get_repeat() { + RepeatSetting::None => RepeatSetting::RepeatPlaylist, + RepeatSetting::RepeatPlaylist => RepeatSetting::RepeatTrack, + RepeatSetting::RepeatTrack => RepeatSetting::None, + }); + + self.queue.set_repeat(mode); + Ok(None) + } + Command::Seek(direction) => { + match *direction { + SeekDirection::Relative(rel) => self.spotify.seek_relative(rel), + SeekDirection::Absolute(abs) => self.spotify.seek(abs), + } + Ok(None) + } + Command::Search(_) + | Command::Move(_, _) + | Command::Shift(_, _) + | Command::Play + | Command::Queue + | Command::Save + | Command::Delete + | Command::Back + | Command::Open + | Command::Goto(_) => Ok(None), + _ => Err("Unknown Command".into()), + } + } + + fn handle_callbacks(&self, s: &mut Cursive, cmd: &Command) -> Result, String> { let local = { let mut main: ViewRef = s.find_id("main").unwrap(); - main.on_command(s, cmd, args)? + main.on_command(s, cmd)? }; if let CommandResult::Consumed(output) = local { @@ -246,24 +140,13 @@ impl CommandManager { }); Ok(None) - } 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()) + self.handle_default_commands(s, cmd) } } - pub fn handle(&self, s: &mut Cursive, cmd: String) { - let components: Vec = cmd - .trim() - .split(' ') - .map(std::string::ToString::to_string) - .collect(); - - let cmd = self.handle_aliases(&components[0]); - let args = components[1..].to_vec(); - - let result = self.handle_callbacks(s, &cmd, &args); + pub fn handle(&self, s: &mut Cursive, cmd: Command) { + let result = self.handle_callbacks(s, &cmd); s.call_on_id("main", |v: &mut Layout| { v.set_result(result); @@ -272,22 +155,21 @@ impl CommandManager { s.on_event(Event::Refresh); } - pub fn register_keybinding, S: Into>( + pub fn register_keybinding>( this: Arc, cursive: &mut Cursive, event: E, - command: S, + command: Command, ) { - let cmd = command.into(); cursive.add_global_callback(event, move |s| { - this.handle(s, cmd.clone()); + this.handle(s, command.clone()); }); } pub fn register_keybindings( this: Arc, cursive: &mut Cursive, - keybindings: Option>, + keybindings: Option>, ) { let mut kb = Self::default_keybindings(); kb.extend(keybindings.unwrap_or_default()); @@ -301,51 +183,51 @@ impl CommandManager { } } - fn default_keybindings() -> HashMap { + fn default_keybindings() -> HashMap { let mut kb = HashMap::new(); - kb.insert("q".into(), "quit".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".into()); - kb.insert("Enter".into(), "play".into()); - kb.insert("s".into(), "save".into()); - kb.insert("Ctrl+s".into(), "save queue".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("x".into(), "share current".into()); - kb.insert("Shift+x".into(), "share selected".into()); + kb.insert("q".into(), Command::Quit); + kb.insert("P".into(), Command::TogglePlay); + kb.insert("R".into(), Command::Playlists(PlaylistCommands::Update)); + kb.insert("S".into(), Command::Stop); + kb.insert("<".into(), Command::Previous); + kb.insert(">".into(), Command::Next); + kb.insert("c".into(), Command::Clear); + kb.insert(" ".into(), Command::Queue); + kb.insert("Enter".into(), Command::Play); + kb.insert("s".into(), Command::Save); + kb.insert("Ctrl+s".into(), Command::SaveQueue); + kb.insert("d".into(), Command::Delete); + kb.insert("/".into(), Command::Focus("search".into())); + kb.insert(".".into(), Command::Seek(SeekDirection::Relative(500))); + kb.insert(",".into(), Command::Seek(SeekDirection::Relative(-500))); + kb.insert("r".into(), Command::Repeat(None)); + kb.insert("z".into(), Command::Shuffle(None)); + kb.insert("x".into(), Command::Share(TargetMode::Current)); + kb.insert("Shift+x".into(), Command::Share(TargetMode::Selected)); - kb.insert("F1".into(), "focus queue".into()); - kb.insert("F2".into(), "focus search".into()); - kb.insert("F3".into(), "focus library".into()); - kb.insert("Backspace".into(), "back".into()); + kb.insert("F1".into(), Command::Focus("queue".into())); + kb.insert("F2".into(), Command::Focus("search".into())); + kb.insert("F3".into(), Command::Focus("library".into())); + kb.insert("Backspace".into(), Command::Back); - kb.insert("o".into(), "open".into()); - kb.insert("a".into(), "goto album".into()); - kb.insert("A".into(), "goto artist".into()); + kb.insert("o".into(), Command::Open); + kb.insert("a".into(), Command::Goto(GotoMode::Album)); + kb.insert("A".into(), Command::Goto(GotoMode::Artist)); - 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("PageUp".into(), "move up 5".into()); - kb.insert("PageDown".into(), "move down 5".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.insert("Up".into(), Command::Move(MoveMode::Up, None)); + kb.insert("Down".into(), Command::Move(MoveMode::Down, None)); + kb.insert("Left".into(), Command::Move(MoveMode::Left, None)); + kb.insert("Right".into(), Command::Move(MoveMode::Right, None)); + kb.insert("PageUp".into(), Command::Move(MoveMode::Up, Some(5))); + kb.insert("PageDown".into(), Command::Move(MoveMode::Down, Some(5))); + kb.insert("k".into(), Command::Move(MoveMode::Up, None)); + kb.insert("j".into(), Command::Move(MoveMode::Down, None)); + kb.insert("h".into(), Command::Move(MoveMode::Left, None)); + kb.insert("l".into(), Command::Move(MoveMode::Right, None)); - kb.insert("Shift+Up".into(), "shift up".into()); - kb.insert("Shift+Down".into(), "shift down".into()); + kb.insert("Shift+Up".into(), Command::Shift(ShiftMode::Up, None)); + kb.insert("Shift+Down".into(), Command::Shift(ShiftMode::Down, None)); kb } diff --git a/src/config.rs b/src/config.rs index 977316b..4318853 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,13 +3,14 @@ use std::fs; use std::path::{Path, PathBuf}; use std::sync::RwLock; +use command::Command; use directories::ProjectDirs; pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b"; #[derive(Serialize, Deserialize, Debug, Default)] pub struct Config { - pub keybindings: Option>, + pub keybindings: Option>, pub theme: Option, pub use_nerdfont: Option, } diff --git a/src/main.rs b/src/main.rs index c78ea94..620ca83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,6 +46,7 @@ use librespot::core::authentication::Credentials; mod album; mod artist; mod authentication; +mod command; mod commands; mod config; mod events; @@ -183,8 +184,8 @@ fn main() { cfg.use_nerdfont.unwrap_or(false), )); - let mut cmd_manager = CommandManager::new(); - cmd_manager.register_all(spotify.clone(), queue.clone(), library.clone()); + let mut cmd_manager = CommandManager::new(spotify.clone(), queue.clone(), library.clone()); + cmd_manager.register_all(); let cmd_manager = Arc::new(cmd_manager); CommandManager::register_keybindings( @@ -240,7 +241,11 @@ fn main() { let mut main = s.find_id::("main").unwrap(); main.clear_cmdline(); } - cmd_manager.handle(s, cmd.to_string()[1..].to_string()); + let c = &cmd[1..]; + let parsed = command::parse(c); + if let Some(parsed) = parsed { + cmd_manager.handle(s, parsed); + } ev.trigger(); }); } diff --git a/src/queue.rs b/src/queue.rs index 97eefd2..d641530 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -5,7 +5,7 @@ use rand::prelude::*; use spotify::Spotify; use track::Track; -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] pub enum RepeatSetting { None, RepeatPlaylist, diff --git a/src/traits.rs b/src/traits.rs index aae2c70..7fde625 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -6,6 +6,7 @@ use cursive::Cursive; use album::Album; use artist::Artist; +use command::Command; use commands::CommandResult; use library::Library; use queue::Queue; @@ -34,25 +35,14 @@ pub trait ViewExt: View { "".into() } - fn on_command( - &mut self, - _s: &mut Cursive, - _cmd: &str, - _args: &[String], - ) -> Result { + fn on_command(&mut self, _s: &mut Cursive, _cmd: &Command) -> Result { Ok(CommandResult::Ignored) } } impl ViewExt for IdView { - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - self.with_view_mut(move |v| v.on_command(s, cmd, args)) - .unwrap() + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + self.with_view_mut(move |v| v.on_command(s, cmd)).unwrap() } } diff --git a/src/ui/album.rs b/src/ui/album.rs index dbb4047..4f2a194 100644 --- a/src/ui/album.rs +++ b/src/ui/album.rs @@ -5,6 +5,7 @@ use cursive::Cursive; use album::Album; use artist::Artist; +use command::Command; use commands::CommandResult; use library::Library; use queue::Queue; @@ -69,12 +70,7 @@ impl ViewExt for AlbumView { format!("{} ({})", self.album.title, self.album.year) } - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - self.tabs.on_command(s, cmd, args) + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + self.tabs.on_command(s, cmd) } } diff --git a/src/ui/artist.rs b/src/ui/artist.rs index e7e8038..6b6716e 100644 --- a/src/ui/artist.rs +++ b/src/ui/artist.rs @@ -5,6 +5,7 @@ use cursive::view::ViewWrapper; use cursive::Cursive; use artist::Artist; +use command::Command; use commands::CommandResult; use library::Library; use queue::Queue; @@ -110,12 +111,7 @@ impl ViewExt for ArtistView { self.artist.name.clone() } - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - self.tabs.on_command(s, cmd, args) + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + self.tabs.on_command(s, cmd) } } diff --git a/src/ui/layout.rs b/src/ui/layout.rs index 849d534..8cdefab 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -12,6 +12,7 @@ use cursive::views::EditView; use cursive::{Cursive, Printer}; use unicode_width::UnicodeWidthStr; +use command::Command; use commands::CommandResult; use events; use traits::{IntoBoxedViewExt, ViewExt}; @@ -205,6 +206,25 @@ impl View for Layout { } } + fn layout(&mut self, size: Vec2) { + self.last_size = size; + + self.statusbar.layout(Vec2::new(size.x, 2)); + + self.cmdline.layout(Vec2::new(size.x, 1)); + + if let Some(screen) = self.get_current_screen_mut() { + screen.view.layout(Vec2::new(size.x, size.y - 3)); + } + + // the focus view has changed, let the views know so they can redraw + // their items + if self.screenchange { + debug!("layout: new screen selected: {:?}", self.focus); + self.screenchange = false; + } + } + fn required_size(&mut self, constraint: Vec2) -> Vec2 { Vec2::new(constraint.x, constraint.y) } @@ -244,25 +264,6 @@ impl View for Layout { } } - fn layout(&mut self, size: Vec2) { - self.last_size = size; - - self.statusbar.layout(Vec2::new(size.x, 2)); - - self.cmdline.layout(Vec2::new(size.x, 1)); - - if let Some(screen) = self.get_current_screen_mut() { - screen.view.layout(Vec2::new(size.x, size.y - 3)); - } - - // the focus view has changed, let the views know so they can redraw - // their items - if self.screenchange { - debug!("layout: new screen selected: {:?}", self.focus); - self.screenchange = false; - } - } - fn call_on_any<'a>(&mut self, s: &Selector, c: AnyCb<'a>) { if let Some(screen) = self.get_current_screen_mut() { screen.view.call_on_any(s, c); @@ -283,29 +284,28 @@ impl View for Layout { } impl ViewExt for Layout { - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - if cmd == "focus" { - if let Some(view) = args.get(0) { + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + match cmd { + Command::Focus(view) => { if self.views.keys().any(|k| k == view) { self.set_view(view.clone()); let screen = self.views.get_mut(view).unwrap(); - screen.view.on_command(s, cmd, args)?; + screen.view.on_command(s, cmd)?; + } + + Ok(CommandResult::Consumed(None)) + } + Command::Back => { + self.pop_view(); + Ok(CommandResult::Consumed(None)) + } + _ => { + if let Some(screen) = self.get_current_screen_mut() { + screen.view.on_command(s, cmd) + } else { + Ok(CommandResult::Ignored) } } - - Ok(CommandResult::Consumed(None)) - } else if cmd == "back" { - self.pop_view(); - Ok(CommandResult::Consumed(None)) - } else if let Some(screen) = self.get_current_screen_mut() { - screen.view.on_command(s, cmd, args) - } else { - Ok(CommandResult::Ignored) } } } diff --git a/src/ui/library.rs b/src/ui/library.rs index c3ab05c..3163d6d 100644 --- a/src/ui/library.rs +++ b/src/ui/library.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use cursive::view::ViewWrapper; use cursive::Cursive; +use command::Command; use commands::CommandResult; use library::Library; use queue::Queue; @@ -48,12 +49,7 @@ impl ViewWrapper for LibraryView { } impl ViewExt for LibraryView { - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - self.tabs.on_command(s, cmd, args) + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + self.tabs.on_command(s, cmd) } } diff --git a/src/ui/listview.rs b/src/ui/listview.rs index f60c221..89defc9 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -10,6 +10,7 @@ use cursive::{Cursive, Printer, Rect, Vec2}; use unicode_width::UnicodeWidthStr; use clipboard::{ClipboardContext, ClipboardProvider}; +use command::{Command, GotoMode, MoveMode, TargetMode}; use commands::CommandResult; use library::Library; use queue::Queue; @@ -277,123 +278,116 @@ impl View for ListView { } impl ViewExt for ListView { - fn on_command( - &mut self, - _s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - if cmd == "play" { - self.queue.clear(); + fn on_command(&mut self, _s: &mut Cursive, cmd: &Command) -> Result { + match cmd { + Command::Play => { + self.queue.clear(); - if !self.attempt_play_all_tracks() { + if !self.attempt_play_all_tracks() { + let mut content = self.content.write().unwrap(); + if let Some(item) = content.get_mut(self.selected) { + item.play(self.queue.clone()); + } + } + + return Ok(CommandResult::Consumed(None)); + } + Command::Queue => { let mut content = self.content.write().unwrap(); if let Some(item) = content.get_mut(self.selected) { - item.play(self.queue.clone()); + item.queue(self.queue.clone()); + } + + return Ok(CommandResult::Consumed(None)); + } + Command::Save => { + let mut item = { + let content = self.content.read().unwrap(); + content.get(self.selected).cloned() + }; + + if let Some(item) = item.as_mut() { + item.toggle_saved(self.library.clone()); } } - return Ok(CommandResult::Consumed(None)); - } - - if cmd == "queue" { - let mut content = self.content.write().unwrap(); - if let Some(item) = content.get_mut(self.selected) { - item.queue(self.queue.clone()); - } - return Ok(CommandResult::Consumed(None)); - } - - if cmd == "save" { - let mut item = { - let content = self.content.read().unwrap(); - content.get(self.selected).cloned() - }; - - if let Some(item) = item.as_mut() { - item.toggle_saved(self.library.clone()); - } - } - - if cmd == "share" { - let source = args.get(0); - let url = - source.and_then(|source| match source.as_str() { - "selected" => self.content.read().ok().and_then(|content| { + Command::Share(mode) => { + let url = match mode { + TargetMode::Selected => self.content.read().ok().and_then(|content| { content.get(self.selected).and_then(ListItem::share_url) }), - "current" => self.queue.get_current().and_then(|t| t.share_url()), - _ => None, - }); + TargetMode::Current => self.queue.get_current().and_then(|t| t.share_url()), + }; - if let Some(url) = url { - ClipboardProvider::new() - .and_then(|mut ctx: ClipboardContext| ctx.set_contents(url)) - .ok(); - }; + if let Some(url) = url { + ClipboardProvider::new() + .and_then(|mut ctx: ClipboardContext| ctx.set_contents(url)) + .ok(); + } - return Ok(CommandResult::Consumed(None)); - } - - if cmd == "move" { - if let Some(dir) = args.get(0) { - let amount: usize = args - .get(1) - .unwrap_or(&"1".to_string()) - .parse() - .map_err(|e| format!("{:?}", e))?; + return Ok(CommandResult::Consumed(None)); + } + Command::Move(mode, amount) => { + let amount = match amount { + Some(amount) => *amount, + _ => 1, + }; let len = self.content.read().unwrap().len(); - if dir == "up" && self.selected > 0 { - self.move_focus(-(amount as i32)); - return Ok(CommandResult::Consumed(None)); - } - - if dir == "down" { - if self.selected < len.saturating_sub(1) { + match mode { + MoveMode::Up if self.selected > 0 => { + self.move_focus(-(amount as i32)); + return Ok(CommandResult::Consumed(None)); + } + MoveMode::Down if self.selected < len.saturating_sub(1) => { self.move_focus(amount as i32); return Ok(CommandResult::Consumed(None)); - } else if self.selected == len.saturating_sub(1) && self.can_paginate() { + } + MoveMode::Down + if self.selected == len.saturating_sub(1) && self.can_paginate() => + { self.pagination.call(&self.content); } + _ => {} } } - } - - if cmd == "open" { - let mut content = self.content.write().unwrap(); - if let Some(item) = content.get_mut(self.selected) { - let queue = self.queue.clone(); - let library = self.library.clone(); - if let Some(view) = item.open(queue, library) { - return Ok(CommandResult::View(view)); - } - } - } - - if cmd == "goto" { - let mut content = self.content.write().unwrap(); - if let Some(item) = content.get_mut(self.selected) { - let queue = self.queue.clone(); - let library = self.library.clone(); - let arg = args.get(0).cloned().unwrap_or_default(); - - if arg == "album" { - if let Some(album) = item.album(queue.clone()) { - let view = AlbumView::new(queue, library, &album).as_boxed_view_ext(); - return Ok(CommandResult::View(view)); - } - } - - if arg == "artist" { - if let Some(artist) = item.artist() { - let view = ArtistView::new(queue, library, &artist).as_boxed_view_ext(); + Command::Open => { + let mut content = self.content.write().unwrap(); + if let Some(item) = content.get_mut(self.selected) { + let queue = self.queue.clone(); + let library = self.library.clone(); + if let Some(view) = item.open(queue, library) { return Ok(CommandResult::View(view)); } } } - } + Command::Goto(mode) => { + let mut content = self.content.write().unwrap(); + if let Some(item) = content.get_mut(self.selected) { + let queue = self.queue.clone(); + let library = self.library.clone(); + + match mode { + GotoMode::Album => { + if let Some(album) = item.album(queue.clone()) { + let view = + AlbumView::new(queue, library, &album).as_boxed_view_ext(); + return Ok(CommandResult::View(view)); + } + } + GotoMode::Artist => { + if let Some(artist) = item.artist() { + let view = + ArtistView::new(queue, library, &artist).as_boxed_view_ext(); + return Ok(CommandResult::View(view)); + } + } + } + } + } + _ => {} + }; Ok(CommandResult::Ignored) } diff --git a/src/ui/playlist.rs b/src/ui/playlist.rs index 70b6667..f3da363 100644 --- a/src/ui/playlist.rs +++ b/src/ui/playlist.rs @@ -3,6 +3,7 @@ use std::sync::{Arc, RwLock}; use cursive::view::ViewWrapper; use cursive::Cursive; +use command::Command; use commands::CommandResult; use library::Library; use playlist::Playlist; @@ -43,12 +44,7 @@ impl ViewExt for PlaylistView { self.playlist.name.clone() } - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - self.list.on_command(s, cmd, args) + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + self.list.on_command(s, cmd) } } diff --git a/src/ui/playlists.rs b/src/ui/playlists.rs index 2912b01..d8b5c57 100644 --- a/src/ui/playlists.rs +++ b/src/ui/playlists.rs @@ -4,6 +4,7 @@ use cursive::view::ViewWrapper; use cursive::views::Dialog; use cursive::Cursive; +use command::Command; use commands::CommandResult; use library::Library; use playlist::Playlist; @@ -52,19 +53,14 @@ impl ViewWrapper for PlaylistsView { } impl ViewExt for PlaylistsView { - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - if cmd == "delete" { + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + if let Command::Delete = cmd { if let Some(dialog) = self.delete_dialog() { s.add_layer(dialog); } return Ok(CommandResult::Consumed(None)); } - self.list.on_command(s, cmd, args) + self.list.on_command(s, cmd) } } diff --git a/src/ui/queue.rs b/src/ui/queue.rs index 78b6873..cad090d 100644 --- a/src/ui/queue.rs +++ b/src/ui/queue.rs @@ -6,6 +6,7 @@ use cursive::Cursive; use std::cmp::min; use std::sync::Arc; +use command::{Command, ShiftMode}; use commands::CommandResult; use library::Library; use queue::Queue; @@ -88,55 +89,52 @@ impl ViewWrapper for QueueView { } impl ViewExt for QueueView { - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - if cmd == "play" { - self.queue.play(self.list.get_selected_index(), true); - return Ok(CommandResult::Consumed(None)); - } + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + match cmd { + Command::Play => { + self.queue.play(self.list.get_selected_index(), true); + return Ok(CommandResult::Consumed(None)); + } + Command::Queue => { + return Ok(CommandResult::Ignored); + } + Command::Delete => { + self.queue.remove(self.list.get_selected_index()); + return Ok(CommandResult::Consumed(None)); + } + Command::Shift(mode, amount) => { + let amount = match amount { + Some(amount) => *amount, + _ => 1, + }; - if cmd == "queue" { - return Ok(CommandResult::Ignored); - } - - if cmd == "delete" { - self.queue.remove(self.list.get_selected_index()); - return Ok(CommandResult::Consumed(None)); - } - - if cmd == "shift" { - if let Some(dir) = args.get(0) { - let amount: usize = args - .get(1) - .unwrap_or(&"1".to_string()) - .parse() - .map_err(|e| format!("{:?}", e))?; let selected = self.list.get_selected_index(); let len = self.queue.len(); - if dir == "up" && selected > 0 { - self.queue.shift(selected, selected.saturating_sub(amount)); - self.list.move_focus(-(amount as i32)); - return Ok(CommandResult::Consumed(None)); - } else if dir == "down" && selected < len.saturating_sub(1) { - self.queue - .shift(selected, min(selected + amount as usize, len - 1)); - self.list.move_focus(amount as i32); - return Ok(CommandResult::Consumed(None)); + + match mode { + ShiftMode::Up if selected > 0 => { + self.queue + .shift(selected, (selected as i32).saturating_sub(amount) as usize); + self.list.move_focus(-(amount as i32)); + return Ok(CommandResult::Consumed(None)); + } + ShiftMode::Down if selected < len.saturating_sub(1) => { + self.queue + .shift(selected, min(selected + amount as usize, len - 1)); + self.list.move_focus(amount as i32); + return Ok(CommandResult::Consumed(None)); + } + _ => {} } } + Command::SaveQueue => { + let dialog = Self::save_dialog(self.queue.clone(), self.library.clone()); + s.add_layer(dialog); + return Ok(CommandResult::Consumed(None)); + } + _ => {} } - if cmd == "save" && args.get(0).unwrap_or(&"".to_string()) == "queue" { - let dialog = Self::save_dialog(self.queue.clone(), self.library.clone()); - s.add_layer(dialog); - return Ok(CommandResult::Consumed(None)); - } - - self.with_view_mut(move |v| v.on_command(s, cmd, args)) - .unwrap() + self.with_view_mut(move |v| v.on_command(s, cmd)).unwrap() } } diff --git a/src/ui/search.rs b/src/ui/search.rs index 9e19853..8bd4eb9 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -11,6 +11,7 @@ use std::sync::{Arc, Mutex, RwLock}; use album::Album; use artist::Artist; +use command::{Command, MoveMode}; use commands::CommandResult; use events::EventManager; use library::Library; @@ -411,6 +412,14 @@ impl View for SearchView { self.tabs.layout(Vec2::new(size.x, size.y - 1)); } + fn on_event(&mut self, event: Event) -> EventResult { + if self.edit_focused { + self.edit.on_event(event) + } else { + self.tabs.on_event(event) + } + } + 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.tabs.call_on_any(selector, Box::new(|v| callback(v))); @@ -424,52 +433,38 @@ impl View for SearchView { Err(()) } } - - fn on_event(&mut self, event: Event) -> EventResult { - if self.edit_focused { - self.edit.on_event(event) - } else { - self.tabs.on_event(event) - } - } } impl ViewExt for SearchView { - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - if cmd == "search" && !args.is_empty() { - self.run_search(args.join(" ")); - return Ok(CommandResult::Consumed(None)); - } - - if cmd == "focus" { - self.edit_focused = true; - self.clear(); - return Ok(CommandResult::Consumed(None)); + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + match cmd { + Command::Search(query) => self.run_search(query.to_string()), + Command::Focus(_) => { + self.edit_focused = true; + self.clear(); + return Ok(CommandResult::Consumed(None)); + } + _ => {} } let result = if !self.edit_focused { - self.tabs.on_command(s, cmd, args)? + self.tabs.on_command(s, cmd)? } else { CommandResult::Ignored }; if let CommandResult::Ignored = result { - if cmd == "move" { - if let Some(dir) = args.get(0) { - if dir == "up" && !self.edit_focused { + if let Command::Move(mode, _) = cmd { + match mode { + MoveMode::Up if !self.edit_focused => { self.edit_focused = true; return Ok(CommandResult::Consumed(None)); } - - if dir == "down" && self.edit_focused { + MoveMode::Down if self.edit_focused => { self.edit_focused = false; return Ok(CommandResult::Consumed(None)); } + _ => {} } } } diff --git a/src/ui/tabview.rs b/src/ui/tabview.rs index 3d71c0d..01f74d8 100644 --- a/src/ui/tabview.rs +++ b/src/ui/tabview.rs @@ -7,6 +7,7 @@ use cursive::traits::View; use cursive::{Cursive, Printer, Vec2}; use unicode_width::UnicodeWidthStr; +use command::{Command, MoveMode}; use commands::CommandResult; use traits::{IntoBoxedViewExt, ViewExt}; @@ -102,36 +103,30 @@ impl View for TabView { } impl ViewExt for TabView { - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - if cmd == "move" { - if let Some(dir) = args.get(0) { - let amount: i32 = args - .get(1) - .unwrap_or(&"1".to_string()) - .parse() - .map_err(|e| format!("{:?}", e))?; + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + if let Command::Move(mode, amount) = cmd { + let amount = match amount { + Some(amount) => *amount, + _ => 1, + }; - let len = self.tabs.len(); + let len = self.tabs.len(); - if dir == "left" && self.selected > 0 { - self.move_focus(-amount); + match mode { + MoveMode::Left if self.selected > 0 => { + self.move_focus(-(amount as i32)); return Ok(CommandResult::Consumed(None)); } - - if dir == "right" && self.selected < len - 1 { - self.move_focus(amount); + MoveMode::Right if self.selected < len - 1 => { + self.move_focus(amount as i32); return Ok(CommandResult::Consumed(None)); } + _ => {} } } if let Some(tab) = self.tabs.get_mut(self.selected) { - tab.view.on_command(s, cmd, args) + tab.view.on_command(s, cmd) } else { Ok(CommandResult::Ignored) }