Add debug log window

This commit is contained in:
Elias Projahn 2023-01-15 13:21:50 +01:00
parent 8b45ec4940
commit 7eb85f094f
8 changed files with 111 additions and 8 deletions

1
Cargo.lock generated
View file

@ -1113,6 +1113,7 @@ dependencies = [
name = "musicus_backend" name = "musicus_backend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"fragile", "fragile",
"gio", "gio",
"glib 0.16.7", "glib 0.16.7",

View file

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
chrono = "0.4"
fragile = "2" fragile = "2"
gio = "0.16" gio = "0.16"
glib = "0.16" glib = "0.16"

View file

@ -5,6 +5,7 @@ use std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
path::PathBuf, path::PathBuf,
rc::Rc, rc::Rc,
sync::Arc,
}; };
use tokio::sync::{broadcast, broadcast::Sender}; use tokio::sync::{broadcast, broadcast::Sender};
@ -18,6 +19,7 @@ pub mod library;
pub use library::*; pub use library::*;
mod logger; mod logger;
pub use logger::{LogMessage, Logger};
pub mod player; pub mod player;
pub use player::*; pub use player::*;
@ -40,6 +42,9 @@ pub enum BackendState {
/// A collection of all backend state and functionality. /// A collection of all backend state and functionality.
pub struct Backend { pub struct Backend {
/// Registered instance of [Logger].
logger: Arc<Logger>,
/// A closure that will be called whenever the backend state changes. /// A closure that will be called whenever the backend state changes.
state_cb: RefCell<Option<Box<dyn Fn(BackendState)>>>, state_cb: RefCell<Option<Box<dyn Fn(BackendState)>>>,
@ -72,11 +77,11 @@ impl Backend {
/// and call init() afterwards. There may be only one backend for a process and this method /// 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. /// may only be called exactly once. Otherwise it will panic.
pub fn new() -> Self { pub fn new() -> Self {
logger::register(); let logger = logger::register();
let (library_updated_sender, _) = broadcast::channel(1024); let (library_updated_sender, _) = broadcast::channel(1024);
Backend { Backend {
logger,
state_cb: RefCell::new(None), state_cb: RefCell::new(None),
settings: gio::Settings::new("de.johrpan.musicus"), settings: gio::Settings::new("de.johrpan.musicus"),
music_library_path: RefCell::new(None), music_library_path: RefCell::new(None),
@ -88,6 +93,11 @@ impl Backend {
} }
} }
/// Get the registered instance of [Logger].
pub fn logger(&self) -> Arc<Logger> {
Arc::clone(&self.logger)
}
/// Set the closure to be called whenever the backend state changes. /// Set the closure to be called whenever the backend state changes.
pub fn set_state_cb<F: Fn(BackendState) + 'static>(&self, cb: F) { pub fn set_state_cb<F: Fn(BackendState) + 'static>(&self, cb: F) {
self.state_cb.replace(Some(Box::new(cb))); self.state_cb.replace(Some(Box::new(cb)));

View file

@ -1,20 +1,31 @@
use chrono::{Local, DateTime};
use log::{Level, LevelFilter, Log, Metadata, Record}; 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. /// Register the custom logger. This will panic if called more than once.
pub fn register() { pub fn register() -> Arc<Logger> {
log::set_boxed_logger(Box::new(Logger::default())) let logger = Arc::new(Logger::default());
log::set_boxed_logger(Box::new(Arc::clone(&logger)))
.map(|()| log::set_max_level(LevelFilter::Info)) .map(|()| log::set_max_level(LevelFilter::Info))
.unwrap(); .unwrap();
logger
} }
/// A simple logging handler that prints out all messages and caches them for /// A simple logging handler that prints out all messages and caches them for
/// later access by the user interface. /// later access by the user interface.
struct Logger { pub struct Logger {
/// All messages since the start of the program. /// All messages since the start of the program.
messages: Mutex<Vec<LogMessage>>, messages: Mutex<Vec<LogMessage>>,
} }
impl Logger {
pub fn messages(&self) -> Vec<LogMessage> {
self.messages.lock().unwrap().clone()
}
}
impl Default for Logger { impl Default for Logger {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -40,7 +51,9 @@ impl Log for Logger {
} }
/// A simplified representation of a [`Record`]. /// A simplified representation of a [`Record`].
struct LogMessage { #[derive(Clone)]
pub struct LogMessage {
pub time: DateTime<Local>,
pub level: String, pub level: String,
pub module: String, pub module: String,
pub message: String, pub message: String,
@ -49,6 +62,7 @@ struct LogMessage {
impl<'a> From<&Record<'a>> for LogMessage { impl<'a> From<&Record<'a>> for LogMessage {
fn from(record: &Record<'a>) -> Self { fn from(record: &Record<'a>) -> Self {
Self { Self {
time: Local::now(),
level: record.level().to_string(), level: record.level().to_string(),
module: String::from(record.module_path().unwrap_or_else(|| record.target())), module: String::from(record.module_path().unwrap_or_else(|| record.target())),
message: format!("{}", record.args()), message: format!("{}", record.args()),
@ -58,6 +72,6 @@ impl<'a> From<&Record<'a>> for LogMessage {
impl Display for LogMessage { impl Display for LogMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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)
} }
} }

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 2 1 c -0.550781 0 -0.992188 0.445312 -0.992188 0.992188 l -0.007812 9.007812 c 0 0.265625 0.105469 0.519531 0.292969 0.707031 s 0.441406 0.292969 0.707031 0.292969 h 2 v -6 c 0 -1.105469 0.894531 -2 2 -2 h 5 v -2 c 0 -0.550781 -0.449219 -1 -1 -1 z m 4 4 c -0.550781 0 -1 0.449219 -1 1 v 9 c 0 0.550781 0.449219 1 1 1 h 6 l 3 -3 v -7 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 535 B

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<gresources> <gresources>
<gresource prefix="/de/johrpan/musicus"> <gresource prefix="/de/johrpan/musicus">
<file preprocess="xml-stripblanks">icons/copy-symbolic.svg</file>
<file preprocess="xml-stripblanks">ui/editor.ui</file> <file preprocess="xml-stripblanks">ui/editor.ui</file>
<file preprocess="xml-stripblanks">ui/import_screen.ui</file> <file preprocess="xml-stripblanks">ui/import_screen.ui</file>
<file preprocess="xml-stripblanks">ui/main_screen.ui</file> <file preprocess="xml-stripblanks">ui/main_screen.ui</file>

View file

@ -150,6 +150,10 @@
<attribute name="label" translatable="yes">Preferences</attribute> <attribute name="label" translatable="yes">Preferences</attribute>
<attribute name="action">widget.preferences</attribute> <attribute name="action">widget.preferences</attribute>
</item> </item>
<item>
<attribute name="label" translatable="yes">Debug log</attribute>
<attribute name="action">widget.log</attribute>
</item>
<item> <item>
<attribute name="label" translatable="yes">About Musicus</attribute> <attribute name="label" translatable="yes">About Musicus</attribute>
<attribute name="action">widget.about</attribute> <attribute name="action">widget.about</attribute>

View file

@ -43,8 +43,10 @@ impl Screen<(), ()> for MainScreen {
let actions = gio::SimpleActionGroup::new(); let actions = gio::SimpleActionGroup::new();
let preferences_action = gio::SimpleAction::new("preferences", None); let preferences_action = gio::SimpleAction::new("preferences", None);
let log_action = gio::SimpleAction::new("log", None);
let about_action = gio::SimpleAction::new("about", None); let about_action = gio::SimpleAction::new("about", None);
actions.add_action(&preferences_action); actions.add_action(&preferences_action);
actions.add_action(&log_action);
actions.add_action(&about_action); actions.add_action(&about_action);
widget.insert_action_group("widget", Some(&actions)); 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(); 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 |_, _| { about_action.connect_activate(clone!(@weak this => move |_, _| {
this.show_about_dialog(); this.show_about_dialog();
})); }));
@ -192,6 +198,70 @@ impl Widget for MainScreen {
} }
impl 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::<Vec<String>>().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(&copy_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!(
"<b>{}</b> {} <i>{}</i>",
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(
&gtk::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. /// Show a dialog with information on this application.
fn show_about_dialog(&self) { fn show_about_dialog(&self) {
let dialog = adw::AboutWindow::builder() let dialog = adw::AboutWindow::builder()