From 6d2a0552bf66bc2bbaf1573d9ef17ec7d5999516 Mon Sep 17 00:00:00 2001 From: Thomas Frans Date: Sun, 28 May 2023 00:52:36 +0200 Subject: [PATCH] refactor: move layout functionality under layout --- src/application.rs | 165 +++++++++--------------------------------- src/ui/layout.rs | 174 +++++++++++++++++++++++++++++++-------------- 2 files changed, 157 insertions(+), 182 deletions(-) diff --git a/src/application.rs b/src/application.rs index 7a472d7..258ea20 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,9 +1,6 @@ use std::path::Path; -use std::rc::Rc; use std::sync::Arc; -use cursive::event::EventTrigger; -use cursive::theme::Theme; use cursive::traits::Nameable; use cursive::{Cursive, CursiveRunner}; use log::{error, info, trace}; @@ -11,15 +8,13 @@ use log::{error, info, trace}; #[cfg(unix)] use signal_hook::{consts::SIGHUP, consts::SIGTERM, iterator::Signals}; -use crate::command::{Command, JumpMode}; +use crate::command::Command; use crate::commands::CommandManager; use crate::config::Config; use crate::events::{Event, EventManager}; -use crate::ext_traits::CursiveExt; use crate::library::Library; use crate::queue::Queue; use crate::spotify::{PlayerEvent, Spotify}; -use crate::ui::contextmenu::ContextMenu; use crate::ui::create_cursive; use crate::{authentication, ui}; use crate::{command, queue, spotify}; @@ -69,14 +64,10 @@ lazy_static!( /// The representation of an ncspot application. pub struct Application { - /// The Spotify library, which is obtained from the Spotify API using rspotify. - library: Arc, /// The music queue which controls playback order. queue: Arc, /// Internally shared spotify: Spotify, - /// The configuration provided in the config file. - configuration: Arc, /// Internally shared event_manager: EventManager, /// An IPC implementation using the D-Bus MPRIS protocol, used to control and inspect ncspot. @@ -87,8 +78,6 @@ pub struct Application { ipc: IpcSocket, /// The object to render to the terminal. cursive: CursiveRunner, - /// The theme used to draw the user interface. - theme: Rc, } impl Application { @@ -112,6 +101,11 @@ impl Application { cursive.set_theme(theme.clone()); + #[cfg(all(unix, feature = "pancurses_backend"))] + cursive.add_global_callback(cursive::event::Event::CtrlChar('z'), |_s| unsafe { + libc::raise(libc::SIGTSTP); + }); + let event_manager = EventManager::new(cursive.cb_sink().clone()); let spotify = @@ -145,57 +139,33 @@ impl Application { ) .map_err(|e| e.to_string())?; - Ok(Self { - library, - queue, - spotify, - configuration, - event_manager, - #[cfg(feature = "mpris")] - mpris_manager, - #[cfg(unix)] - ipc, - cursive, - theme: Rc::new(theme), - }) - } - - pub fn run(&mut self) -> Result<(), String> { let mut cmd_manager = CommandManager::new( - self.spotify.clone(), - self.queue.clone(), - self.library.clone(), - self.configuration.clone(), - self.event_manager.clone(), + spotify.clone(), + queue.clone(), + library.clone(), + configuration.clone(), + event_manager.clone(), ); cmd_manager.register_all(); - cmd_manager.register_keybindings(&mut self.cursive); + cmd_manager.register_keybindings(&mut cursive); - let user_data: UserData = Arc::new(UserDataInner { cmd: cmd_manager }); - self.cursive.set_user_data(user_data); + cursive.set_user_data(Arc::new(UserDataInner { cmd: cmd_manager })); - let search = ui::search::SearchView::new( - self.event_manager.clone(), - self.queue.clone(), - self.library.clone(), - ); + let search = + ui::search::SearchView::new(event_manager.clone(), queue.clone(), library.clone()); - let libraryview = ui::library::LibraryView::new(self.queue.clone(), self.library.clone()); + let libraryview = ui::library::LibraryView::new(queue.clone(), library.clone()); - let queueview = ui::queue::QueueView::new(self.queue.clone(), self.library.clone()); + let queueview = ui::queue::QueueView::new(queue.clone(), library.clone()); #[cfg(feature = "cover")] - let coverview = ui::cover::CoverView::new( - self.queue.clone(), - self.library.clone(), - &self.configuration, - ); + let coverview = ui::cover::CoverView::new(queue.clone(), library.clone(), &configuration); - let status = ui::statusbar::StatusBar::new(self.queue.clone(), Arc::clone(&self.library)); + let status = ui::statusbar::StatusBar::new(queue.clone(), Arc::clone(&library)); let mut layout = - ui::layout::Layout::new(status, &self.event_manager, Rc::clone(&self.theme)) + ui::layout::Layout::new(status, &event_manager, theme, Arc::clone(&configuration)) .screen("search", search.with_name("search")) .screen("library", libraryview.with_name("library")) .screen("queue", queueview); @@ -204,8 +174,7 @@ impl Application { layout.add_screen("cover", coverview.with_name("cover")); // initial screen is library - let initial_screen = self - .configuration + let initial_screen = configuration .values() .initial_screen .clone() @@ -217,86 +186,22 @@ impl Application { layout.set_screen("library"); } - let cmd_key = |cfg: Arc| cfg.values().command_key.unwrap_or(':'); + cursive.add_fullscreen_layer(layout.with_name("main")); - { - let c = self.configuration.clone(); - let config_clone = Arc::clone(&self.configuration); - self.cursive.set_on_post_event( - EventTrigger::from_fn(move |event| { - event == &cursive::event::Event::Char(cmd_key(c.clone())) - }), - move |s| { - if s.find_name::("contextmenu").is_none() { - s.call_on_name("main", |v: &mut ui::layout::Layout| { - v.enable_cmdline(cmd_key(config_clone.clone())); - }); - } - }, - ); - } - - self.cursive.add_global_callback('/', move |s| { - if s.find_name::("contextmenu").is_none() { - s.call_on_name("main", |v: &mut ui::layout::Layout| { - v.enable_jump(); - }); - } - }); - - self.cursive - .add_global_callback(cursive::event::Key::Esc, move |s| { - if s.find_name::("contextmenu").is_none() { - s.call_on_name("main", |v: &mut ui::layout::Layout| { - v.clear_cmdline(); - }); - } - }); - - layout.cmdline.set_on_edit(move |s, cmd, _| { - s.call_on_name("main", |v: &mut ui::layout::Layout| { - if cmd.is_empty() { - v.clear_cmdline(); - } - }); - }); - - { - let ev = self.event_manager.clone(); - layout.cmdline.set_on_submit(move |s, cmd| { - s.on_layout(|_, mut layout| layout.clear_cmdline()); - let cmd_without_prefix = &cmd[1..]; - if cmd.strip_prefix('/').is_some() { - let command = Command::Jump(JumpMode::Query(cmd_without_prefix.to_string())); - if let Some(data) = s.user_data::().cloned() { - data.cmd.handle(s, command); - } - } else { - match command::parse(cmd_without_prefix) { - Ok(commands) => { - if let Some(data) = s.user_data::().cloned() { - for cmd in commands { - data.cmd.handle(s, cmd); - } - } - } - Err(err) => { - s.on_layout(|_, mut layout| layout.set_result(Err(err.to_string()))); - } - } - } - ev.trigger(); - }); - } - - self.cursive.add_fullscreen_layer(layout.with_name("main")); - - #[cfg(all(unix, feature = "pancurses_backend"))] - self.cursive - .add_global_callback(cursive::event::Event::CtrlChar('z'), |_s| unsafe { - libc::raise(libc::SIGTSTP); - }); + Ok(Self { + queue, + spotify, + event_manager, + #[cfg(feature = "mpris")] + mpris_manager, + #[cfg(unix)] + ipc, + cursive, + }) + } + /// Start the application and run the event loop. + pub fn run(&mut self) -> Result<(), String> { #[cfg(unix)] let mut signals = Signals::new([SIGTERM, SIGHUP]).expect("could not register signal handler"); diff --git a/src/ui/layout.rs b/src/ui/layout.rs index 9658ca4..9fb7df7 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -1,10 +1,10 @@ use std::collections::HashMap; -use std::rc::Rc; +use std::sync::Arc; use std::time::{Duration, SystemTime}; use cursive::align::HAlign; use cursive::direction::Direction; -use cursive::event::{AnyCb, Event, EventResult, MouseButton, MouseEvent}; +use cursive::event::{AnyCb, Event, EventResult, Key, MouseButton, MouseEvent}; use cursive::theme::{ColorStyle, ColorType, Theme}; use cursive::traits::View; use cursive::vec::Vec2; @@ -14,9 +14,12 @@ use cursive::{Cursive, Printer}; use log::debug; use unicode_width::UnicodeWidthStr; -use crate::command::Command; +use crate::application::UserData; +use crate::command::{self, Command, JumpMode}; use crate::commands::CommandResult; +use crate::config::Config; use crate::events; +use crate::ext_traits::CursiveExt; use crate::traits::{IntoBoxedViewExt, ViewExt}; pub struct Layout { @@ -24,29 +27,74 @@ pub struct Layout { stack: HashMap>>, statusbar: Box, focus: Option, - pub cmdline: EditView, + cmdline: EditView, cmdline_focus: bool, result: Result, String>, result_time: Option, screenchange: bool, last_size: Vec2, ev: events::EventManager, - theme: Rc, + theme: Theme, + configuration: Arc, } impl Layout { - pub fn new(status: T, ev: &events::EventManager, theme: Rc) -> Layout { + pub fn new( + status: T, + ev: &events::EventManager, + theme: Theme, + configuration: Arc, + ) -> Layout { let style = ColorStyle::new( ColorType::Color(*theme.palette.custom("cmdline_bg").unwrap()), ColorType::Color(*theme.palette.custom("cmdline").unwrap()), ); + let mut command_line_input = EditView::new().filler(" ").style(style); + + let event_manager = ev.clone(); + // 1. When a search was submitted on the commandline... + command_line_input.set_on_submit(move |s, cmd| { + // 2. Clear the commandline on Layout... + s.on_layout(|_, mut layout| layout.clear_cmdline()); + + // 3. Get the actual command without the prefix (like `:` or `/`)... + let cmd_without_prefix = &cmd[1..]; + if cmd.strip_prefix('/').is_some() { + // 4. If it is a search command... + + // 5. Send a jump command with the search query to the command manager. + let command = Command::Jump(JumpMode::Query(cmd_without_prefix.to_string())); + if let Some(data) = s.user_data::().cloned() { + data.cmd.handle(s, command); + } + } else { + // 4. If it is an actual command... + + // 5. Parse the command and... + match command::parse(cmd_without_prefix) { + Ok(commands) => { + // 6. Send the parsed command to the command manager. + if let Some(data) = s.user_data::().cloned() { + for cmd in commands { + data.cmd.handle(s, cmd); + } + } + } + Err(err) => { + // 6. Set an error message on the global layout. + s.on_layout(|_, mut layout| layout.set_result(Err(err.to_string()))); + } + } + } + event_manager.trigger(); + }); Layout { screens: HashMap::new(), stack: HashMap::new(), statusbar: status.into_boxed_view(), focus: None, - cmdline: EditView::new().filler(" ").style(style), + cmdline: command_line_input, cmdline_focus: false, result: Ok(None), result_time: None, @@ -54,6 +102,7 @@ impl Layout { last_size: Vec2::new(0, 0), ev: ev.clone(), theme, + configuration, } } @@ -290,57 +339,78 @@ impl View for Layout { } fn on_event(&mut self, event: Event) -> EventResult { - // handle mouse events in cmdline/statusbar area - if let Event::Mouse { - position, - event: mouseevent, - .. - } = event - { - if position.y == 0 { - if mouseevent == MouseEvent::Press(MouseButton::Left) - && !self.is_current_stack_empty() - && position.x - < self - .get_current_screen() - .map(|screen| screen.title()) - .unwrap_or_default() - .len() - + 3 - { - self.pop_view(); + match event { + Event::Char(':') => { + let result = if let Some(view) = self.get_current_view_mut() { + view.on_event(event.relativized((0, 1))) + } else { + EventResult::Ignored + }; + + if let EventResult::Ignored = result { + let command_key = self.configuration.values().command_key.unwrap_or(':'); + self.enable_cmdline(command_key); + } else { + return EventResult::Ignored; } - return EventResult::consumed(); } - - let result = self.get_result(); - - let cmdline_visible = self.cmdline.get_content().len() > 0; - let mut cmdline_height = usize::from(cmdline_visible); - if result.as_ref().map(Option::is_some).unwrap_or(true) { - cmdline_height += 1; + Event::Char('/') => self.enable_jump(), + Event::Key(Key::Esc) if self.cmdline_focus => self.clear_cmdline(), + _ if self.cmdline_focus => { + let result = self.cmdline.on_event(event); + if self.cmdline.get_content().is_empty() { + self.clear_cmdline(); + } + return result; } + Event::Mouse { + position, + event: mouse_event, + .. + } => { + // Handle mouse events in the command/jump area. + if position.y == 0 { + if mouse_event == MouseEvent::Press(MouseButton::Left) + && !self.is_current_stack_empty() + && position.x + < self + .get_current_screen() + .map(|screen| screen.title()) + .unwrap_or_default() + .len() + + 3 + { + self.pop_view(); + } + return EventResult::consumed(); + } - if position.y >= self.last_size.y.saturating_sub(2 + cmdline_height) - && position.y < self.last_size.y - cmdline_height - { - self.statusbar.on_event( - event.relativized(Vec2::new(0, self.last_size.y - 2 - cmdline_height)), - ); - return EventResult::Consumed(None); + let result = self.get_result(); + + let cmdline_visible = self.cmdline.get_content().len() > 0; + let mut cmdline_height = usize::from(cmdline_visible); + if result.as_ref().map(Option::is_some).unwrap_or(true) { + cmdline_height += 1; + } + + if position.y >= self.last_size.y.saturating_sub(2 + cmdline_height) + && position.y < self.last_size.y - cmdline_height + { + self.statusbar.on_event( + event.relativized(Vec2::new(0, self.last_size.y - 2 - cmdline_height)), + ); + return EventResult::Consumed(None); + } + } + _ => { + if let Some(view) = self.get_current_view_mut() { + return view.on_event(event.relativized((0, 1))); + } else { + return EventResult::Ignored; + } } } - - if self.cmdline_focus { - debug!("cmdline event"); - return self.cmdline.on_event(event); - } - - if let Some(view) = self.get_current_view_mut() { - view.on_event(event.relativized((0, 1))) - } else { - EventResult::Ignored - } + EventResult::Consumed(None) } fn call_on_any(&mut self, s: &Selector, c: AnyCb<'_>) {