diff --git a/Cargo.lock b/Cargo.lock index ec68ea1..c091f2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1113,6 +1113,7 @@ dependencies = [ name = "musicus_backend" version = "0.1.0" dependencies = [ + "chrono", "fragile", "gio", "glib 0.16.7", diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index 5e6b453..4fd886b 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +chrono = "0.4" fragile = "2" gio = "0.16" glib = "0.16" diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index 06fe06a..b0d5579 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -5,6 +5,7 @@ use std::{ cell::{Cell, RefCell}, path::PathBuf, rc::Rc, + sync::Arc, }; use tokio::sync::{broadcast, broadcast::Sender}; @@ -18,6 +19,7 @@ pub mod library; pub use library::*; mod logger; +pub use logger::{LogMessage, Logger}; pub mod player; pub use player::*; @@ -40,6 +42,9 @@ pub enum BackendState { /// A collection of all backend state and functionality. pub struct Backend { + /// Registered instance of [Logger]. + logger: Arc, + /// A closure that will be called whenever the backend state changes. state_cb: RefCell>>, @@ -72,11 +77,11 @@ impl Backend { /// and call init() afterwards. There may be only one backend for a process and this method /// may only be called exactly once. Otherwise it will panic. pub fn new() -> Self { - logger::register(); - + let logger = logger::register(); let (library_updated_sender, _) = broadcast::channel(1024); Backend { + logger, state_cb: RefCell::new(None), settings: gio::Settings::new("de.johrpan.musicus"), music_library_path: RefCell::new(None), @@ -88,6 +93,11 @@ impl Backend { } } + /// Get the registered instance of [Logger]. + pub fn logger(&self) -> Arc { + Arc::clone(&self.logger) + } + /// Set the closure to be called whenever the backend state changes. pub fn set_state_cb(&self, cb: F) { self.state_cb.replace(Some(Box::new(cb))); diff --git a/crates/backend/src/logger.rs b/crates/backend/src/logger.rs index 731caa3..d0fe413 100644 --- a/crates/backend/src/logger.rs +++ b/crates/backend/src/logger.rs @@ -1,20 +1,31 @@ +use chrono::{Local, DateTime}; use log::{Level, LevelFilter, Log, Metadata, Record}; -use std::{fmt::Display, sync::Mutex}; +use std::{fmt::Display, sync::{Arc, Mutex}}; /// Register the custom logger. This will panic if called more than once. -pub fn register() { - log::set_boxed_logger(Box::new(Logger::default())) +pub fn register() -> Arc { + let logger = Arc::new(Logger::default()); + + log::set_boxed_logger(Box::new(Arc::clone(&logger))) .map(|()| log::set_max_level(LevelFilter::Info)) .unwrap(); + + logger } /// A simple logging handler that prints out all messages and caches them for /// later access by the user interface. -struct Logger { +pub struct Logger { /// All messages since the start of the program. messages: Mutex>, } +impl Logger { + pub fn messages(&self) -> Vec { + self.messages.lock().unwrap().clone() + } +} + impl Default for Logger { fn default() -> Self { Self { @@ -40,7 +51,9 @@ impl Log for Logger { } /// A simplified representation of a [`Record`]. -struct LogMessage { +#[derive(Clone)] +pub struct LogMessage { + pub time: DateTime, pub level: String, pub module: String, pub message: String, @@ -49,6 +62,7 @@ struct LogMessage { impl<'a> From<&Record<'a>> for LogMessage { fn from(record: &Record<'a>) -> Self { Self { + time: Local::now(), level: record.level().to_string(), module: String::from(record.module_path().unwrap_or_else(|| record.target())), message: format!("{}", record.args()), @@ -58,6 +72,6 @@ impl<'a> From<&Record<'a>> for LogMessage { impl Display for LogMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} ({}): {}", self.module, self.level, self.message) + write!(f, "{} {} ({}): {}", self.time, self.module, self.level, self.message) } } diff --git a/crates/musicus/res/icons/copy-symbolic.svg b/crates/musicus/res/icons/copy-symbolic.svg new file mode 100644 index 0000000..e633938 --- /dev/null +++ b/crates/musicus/res/icons/copy-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/crates/musicus/res/musicus.gresource.xml b/crates/musicus/res/musicus.gresource.xml index 57f73f4..16da56b 100644 --- a/crates/musicus/res/musicus.gresource.xml +++ b/crates/musicus/res/musicus.gresource.xml @@ -1,6 +1,7 @@ + icons/copy-symbolic.svg ui/editor.ui ui/import_screen.ui ui/main_screen.ui diff --git a/crates/musicus/res/ui/main_screen.ui b/crates/musicus/res/ui/main_screen.ui index 50765c1..8cc4100 100644 --- a/crates/musicus/res/ui/main_screen.ui +++ b/crates/musicus/res/ui/main_screen.ui @@ -150,6 +150,10 @@ Preferences widget.preferences + + Debug log + widget.log + About Musicus widget.about diff --git a/crates/musicus/src/screens/main.rs b/crates/musicus/src/screens/main.rs index a7b8698..d4c09d4 100644 --- a/crates/musicus/src/screens/main.rs +++ b/crates/musicus/src/screens/main.rs @@ -43,8 +43,10 @@ impl Screen<(), ()> for MainScreen { let actions = gio::SimpleActionGroup::new(); let preferences_action = gio::SimpleAction::new("preferences", None); + let log_action = gio::SimpleAction::new("log", None); let about_action = gio::SimpleAction::new("about", None); actions.add_action(&preferences_action); + actions.add_action(&log_action); actions.add_action(&about_action); widget.insert_action_group("widget", Some(&actions)); @@ -76,6 +78,10 @@ impl Screen<(), ()> for MainScreen { Preferences::new(Rc::clone(&this.handle.backend), &this.handle.window).show(); })); + log_action.connect_activate(clone!(@weak this => move |_, _| { + this.show_log_window(); + })); + about_action.connect_activate(clone!(@weak this => move |_, _| { this.show_about_dialog(); })); @@ -192,6 +198,70 @@ impl Widget for MainScreen { } impl MainScreen { + /// Show a window displaying all currently cached log messages. + fn show_log_window(&self) { + let copy_button = gtk::Button::builder().icon_name("copy-symbolic").build(); + let logger = self.handle.backend.logger(); + let toast_overlay = adw::ToastOverlay::new(); + + copy_button.connect_clicked(clone!(@weak logger, @weak toast_overlay => move |widget| { + widget.clipboard().set_text(&logger.messages().into_iter().map(|m| m.to_string()).collect::>().join("\n")); + toast_overlay.add_toast(&adw::Toast::builder().title(&gettext("Copied to clipboard")).build()); + })); + + let header = adw::HeaderBar::builder() + .title_widget( + &adw::WindowTitle::builder() + .title(&gettext("Debug log")) + .build(), + ) + .build(); + + header.pack_end(©_button); + + let log_list = gtk::ListBox::builder() + .selection_mode(gtk::SelectionMode::None) + .build(); + + for message in logger.messages() { + log_list.append( + &adw::ActionRow::builder() + .title(&format!( + "{} {} {}", + message.level, + message.time.format("%Y-%m-%d %H:%M:%S"), + message.module + )) + .subtitle(&message.message) + .build(), + ); + } + + let content = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + + content.append(&header); + content.append( + >k::ScrolledWindow::builder() + .vexpand(true) + .child(&log_list) + .build(), + ); + + toast_overlay.set_child(Some(&content)); + + adw::Window::builder() + .transient_for(&self.handle.window) + .modal(true) + .title(&gettext("Debug log")) + .default_width(640) + .default_height(480) + .content(&toast_overlay) + .build() + .show(); + } + /// Show a dialog with information on this application. fn show_about_dialog(&self) { let dialog = adw::AboutWindow::builder()