diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..fa0cfcc --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,49 @@ +use std::collections::HashMap; + +use cursive::Cursive; + +pub struct CommandManager { + commands: HashMap) -> Result, String>>>, + aliases: HashMap, +} + +impl CommandManager { + pub fn new() -> CommandManager { + CommandManager { + commands: HashMap::new(), + aliases: HashMap::new(), + } + } + + pub fn register>( + &mut self, + name: S, + aliases: Vec, + cb: Box) -> Result, String>> + ) { + let name = name.into(); + for a in aliases { + self.aliases.insert(a.into(), name.clone()); + } + self.commands.insert(name, cb); + } + + fn handle_aliases(&self, name: &String) -> String { + if let Some(s) = self.aliases.get(name) { + self.handle_aliases(s) + } else { + name.clone() + } + } + + pub fn handle(&self, s: &mut Cursive, cmd: String) -> Result, String> { + // TODO: handle quoted arguments + let components: Vec = cmd.split(' ').map(|s| s.to_string()).collect(); + + if let Some(cb) = self.commands.get(&self.handle_aliases(&components[0])) { + cb(s, components[1..].to_vec()) + } else { + Err("Unknown command.".to_string()) + } + } +} diff --git a/src/events.rs b/src/events.rs index 9471eed..2882590 100644 --- a/src/events.rs +++ b/src/events.rs @@ -9,6 +9,7 @@ pub enum Event { Queue(QueueEvent), Player(PlayerEvent), Playlist(PlaylistEvent), + Command(String), } pub type EventSender = Sender; diff --git a/src/main.rs b/src/main.rs index 226c9d0..f1c41f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,8 +25,8 @@ use std::sync::Arc; use std::sync::Mutex; use cursive::event::Key; -use cursive::traits::Identifiable; -use cursive::view::ScrollStrategy; +use cursive::traits::{Identifiable, View}; +use cursive::view::{Selector, ScrollStrategy}; use cursive::views::*; use cursive::Cursive; @@ -37,11 +37,13 @@ mod spotify; mod theme; mod track; mod ui; +mod commands; use events::{Event, EventManager}; use queue::QueueEvent; use spotify::PlayerEvent; use ui::playlist::PlaylistEvent; +use commands::{CommandManager}; fn init_logger(content: TextContent) { let mut builder = env_logger::Builder::from_default_env(); @@ -67,6 +69,19 @@ fn init_logger(content: TextContent) { } } +fn register_keybinding, S: Into>( + cursive: &mut Cursive, + ev: &EventManager, + event: E, + command: S +) { + let ev = ev.clone(); + let cmd = command.into(); + cursive.add_global_callback(event, move |_s| { + ev.send(Event::Command(cmd.clone())); + }); +} + fn main() { std::env::set_var("RUST_LOG", "ncspot=trace"); std::env::set_var("RUST_BACKTRACE", "full"); @@ -101,12 +116,12 @@ fn main() { init_logger(logbuf); let mut cursive = Cursive::default(); - let event_manager = EventManager::new(cursive.cb_sink().clone()); - - cursive.add_global_callback('q', |s| s.quit()); cursive.set_theme(theme::default()); cursive.set_autorefresh(true); + let event_manager = EventManager::new(cursive.cb_sink().clone()); + let mut cmd_manager = CommandManager::new(); + let spotify = Arc::new(spotify::Spotify::new( event_manager.clone(), cfg.username, @@ -119,36 +134,6 @@ fn main() { spotify.clone(), ))); - // global player keybindings (play, pause, stop) - { - let queue = queue.clone(); - cursive.add_global_callback('P', move |_s| { - queue.lock().expect("could not lock queue").toggleplayback(); - }); - } - - { - let queue = queue.clone(); - cursive.add_global_callback('S', move |_s| { - queue.lock().expect("could not lock queue").stop(); - }); - } - - { - let queue = queue.clone(); - cursive.add_global_callback('>', move |_s| { - queue.lock().expect("could not lock queue").next(); - }); - } - - { - let queue = queue.clone(); - cursive.add_global_callback('c', move |_s| { - let mut queue = queue.lock().expect("could not lock queue"); - queue.clear(); - }); - } - let search = ui::search::SearchView::new(spotify.clone(), queue.clone()); let mut playlists = @@ -160,45 +145,122 @@ fn main() { let status = ui::statusbar::StatusBar::new(queue.clone(), spotify.clone()); - let layout = ui::layout::Layout::new(status) - .view("search", BoxView::with_full_height(search.view), "Search") + let mut layout = ui::layout::Layout::new(status) + .view("search", search.view.with_id("search"), "Search") .view("log", logview_scroller, "Log") .view("playlists", playlists.view.take().unwrap(), "Playlists") .view("queue", queueview.view.take().unwrap(), "Queue"); - cursive.add_fullscreen_layer(layout.with_id("main")); + cursive.add_global_callback(':', move |s| { + s.call_on_id("main", |v: &mut ui::layout::Layout| { + v.enable_cmdline(); + }); + }); { let ev = event_manager.clone(); - cursive.add_global_callback(Key::F1, move |s| { + layout.cmdline.set_on_submit(move |s, cmd| { + s.call_on_id("main", |v: &mut ui::layout::Layout| { + v.clear_cmdline(); + ev.send(Event::Command(cmd.to_string()[1..].to_string())); + }); + }); + } + + cursive.add_fullscreen_layer(layout.with_id("main")); + + // Register commands + cmd_manager.register("quit", vec!["q", "x"], Box::new(move |s, _args| { + s.quit(); + Ok(None) + })); + + { + let queue = queue.clone(); + cmd_manager.register("toggleplayback", vec!["toggleplay", "toggle", "play", "pause"], Box::new(move |_s, _args| { + queue.lock().expect("could not lock queue").toggleplayback(); + Ok(None) + })); + } + + { + let queue = queue.clone(); + cmd_manager.register("stop", Vec::new(), Box::new(move |_s, _args| { + queue.lock().expect("could not lock queue").stop(); + Ok(None) + })); + } + + { + let 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) + })); + } + + { + let ev = event_manager.clone(); + 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"); }); ev.send(Event::Queue(QueueEvent::Show)); - }); + Ok(None) + })); } - cursive.add_global_callback(Key::F2, move |s| { + 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[0].clone()); + }); + } + Ok(None) + })); { let ev = event_manager.clone(); - cursive.add_global_callback(Key::F3, move |s| { + 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"); }); ev.send(Event::Playlist(PlaylistEvent::Show)); - }); + Ok(None) + })); } - cursive.add_global_callback(Key::F9, move |s| { + 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, '>', "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 while cursive.is_running() { @@ -212,8 +274,16 @@ fn main() { queue.lock().expect("could not lock queue").next(); } spotify.update_status(state); - } + }, Event::Playlist(event) => playlists.handle_ev(&mut cursive, event), + Event::Command(cmd) => { + // TODO: handle non-error output as well + if let Err(e) = cmd_manager.handle(&mut cursive, cmd) { + cursive.call_on_id("main", |v: &mut ui::layout::Layout| { + v.set_error(e); + }); + } + }, } } } diff --git a/src/ui/layout.rs b/src/ui/layout.rs index 9910c55..8b938e5 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -1,3 +1,4 @@ +use std::time::{SystemTime, Duration}; use std::collections::HashMap; use cursive::align::HAlign; @@ -7,6 +8,7 @@ use cursive::theme::ColorStyle; use cursive::traits::View; use cursive::vec::Vec2; use cursive::view::{IntoBoxedView, Selector}; +use cursive::views::EditView; use cursive::Printer; use unicode_width::UnicodeWidthStr; @@ -20,6 +22,10 @@ pub struct Layout { title: String, statusbar: Box, focus: Option, + pub cmdline: EditView, + cmdline_focus: bool, + error: Option, + error_time: Option, } impl Layout { @@ -29,6 +35,17 @@ impl Layout { title: String::new(), statusbar: status.as_boxed_view(), focus: None, + cmdline: EditView::new().filler(" "), + cmdline_focus: false, + error: None, + error_time: None, + } + } + + pub fn enable_cmdline(&mut self) { + if !self.cmdline_focus { + self.cmdline.set_content(":"); + self.cmdline_focus = true; } } @@ -53,11 +70,41 @@ impl Layout { let title = &self.views.get(&s).unwrap().title; self.title = title.clone(); self.focus = Some(s); + self.cmdline_focus = false; + } + + pub fn set_error>(&mut self, error: S) { + self.error = Some(error.into()); + self.error_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; + } + + fn get_error(&self) -> Option { + if let Some(t) = self.error_time { + if t.elapsed().unwrap() > Duration::from_secs(5) { + return None; + } + } + self.error.clone() } } impl View for Layout { fn draw(&self, printer: &Printer<'_, '_>) { + let error = self.get_error(); + + let cmdline_visible = self.cmdline.get_content().len() > 0; + let mut cmdline_height = if cmdline_visible { 1 } else { 0 }; + if error.is_some() { + cmdline_height += 1; + } + // screen title printer.with_color(ColorStyle::title_primary(), |printer| { let offset = HAlign::Center.get_offset(self.title.width(), printer.size.x); @@ -69,13 +116,26 @@ impl View for Layout { let screen = self.views.get(id).unwrap(); let printer = &printer .offset((0, 1)) - .cropped((printer.size.x, printer.size.y - 3)) + .cropped((printer.size.x, printer.size.y - 3 - cmdline_height)) .focused(true); screen.view.draw(printer); } self.statusbar - .draw(&printer.offset((0, printer.size.y - 2))); + .draw(&printer.offset((0, printer.size.y - 2 - cmdline_height))); + + if let Some(e) = error { + printer.with_color(ColorStyle::highlight(), |printer| { + printer.print_hline((0, printer.size.y - cmdline_height), printer.size.x, " "); + printer.print((0, printer.size.y - cmdline_height), &format!("ERROR: {}", e)); + }); + } + + if cmdline_visible { + let printer = &printer + .offset((0, printer.size.y - 1)); + self.cmdline.draw(&printer); + } } fn required_size(&mut self, constraint: Vec2) -> Vec2 { @@ -83,6 +143,10 @@ impl View for Layout { } fn on_event(&mut self, event: Event) -> EventResult { + if self.cmdline_focus { + return self.cmdline.on_event(event); + } + if let Some(ref id) = self.focus { let screen = self.views.get_mut(id).unwrap(); screen.view.on_event(event) @@ -92,6 +156,8 @@ impl View for Layout { } fn layout(&mut self, size: Vec2) { + self.cmdline.layout(Vec2::new(size.x, 1)); + if let Some(ref id) = self.focus { let screen = self.views.get_mut(id).unwrap(); screen.view.layout(Vec2::new(size.x, size.y - 3)); @@ -106,6 +172,10 @@ impl View for Layout { } fn take_focus(&mut self, source: Direction) -> bool { + if self.cmdline_focus { + return self.cmdline.take_focus(source); + } + if let Some(ref id) = self.focus { let screen = self.views.get_mut(id).unwrap(); screen.view.take_focus(source)