refactor: move layout functionality under layout

This commit is contained in:
Thomas Frans
2023-05-28 00:52:36 +02:00
committed by Henrik Friedrichsen
parent d0efc0868f
commit 6d2a0552bf
2 changed files with 157 additions and 182 deletions

View File

@@ -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<Library>,
/// The music queue which controls playback order.
queue: Arc<Queue>,
/// Internally shared
spotify: Spotify,
/// The configuration provided in the config file.
configuration: Arc<Config>,
/// 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<Cursive>,
/// The theme used to draw the user interface.
theme: Rc<Theme>,
}
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<Config>| 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>("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>("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>("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::<UserData>().cloned() {
data.cmd.handle(s, command);
}
} else {
match command::parse(cmd_without_prefix) {
Ok(commands) => {
if let Some(data) = s.user_data::<UserData>().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");

View File

@@ -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<String, Vec<Box<dyn ViewExt>>>,
statusbar: Box<dyn View>,
focus: Option<String>,
pub cmdline: EditView,
cmdline: EditView,
cmdline_focus: bool,
result: Result<Option<String>, String>,
result_time: Option<SystemTime>,
screenchange: bool,
last_size: Vec2,
ev: events::EventManager,
theme: Rc<Theme>,
theme: Theme,
configuration: Arc<Config>,
}
impl Layout {
pub fn new<T: IntoBoxedView>(status: T, ev: &events::EventManager, theme: Rc<Theme>) -> Layout {
pub fn new<T: IntoBoxedView>(
status: T,
ev: &events::EventManager,
theme: Theme,
configuration: Arc<Config>,
) -> 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::<UserData>().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::<UserData>().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<'_>) {