Refactor command handling

This commit is contained in:
KoffeinFlummi
2019-03-28 03:05:25 +01:00
parent 83a394790f
commit 486bc7617e
10 changed files with 380 additions and 349 deletions

View File

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

View File

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

View File

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

View File

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

View File

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