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"
version = "0.1.0"
dependencies = [
"chrono",
"fragile",
"gio",
"glib 0.16.7",

View file

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

View file

@ -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<Logger>,
/// A closure that will be called whenever the backend state changes.
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
/// 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<Logger> {
Arc::clone(&self.logger)
}
/// Set the closure to be called whenever the backend state changes.
pub fn set_state_cb<F: Fn(BackendState) + 'static>(&self, cb: F) {
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 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<Logger> {
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<Vec<LogMessage>>,
}
impl Logger {
pub fn messages(&self) -> Vec<LogMessage> {
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<Local>,
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)
}
}

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"?>
<gresources>
<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/import_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="action">widget.preferences</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Debug log</attribute>
<attribute name="action">widget.log</attribute>
</item>
<item>
<attribute name="label" translatable="yes">About Musicus</attribute>
<attribute name="action">widget.about</attribute>

View file

@ -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::<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.
fn show_about_dialog(&self) {
let dialog = adw::AboutWindow::builder()