refactor: move layout functionality under layout
This commit is contained in:
committed by
Henrik Friedrichsen
parent
d0efc0868f
commit
6d2a0552bf
@@ -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");
|
||||
|
||||
174
src/ui/layout.rs
174
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<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<'_>) {
|
||||
|
||||
Reference in New Issue
Block a user