mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 11:47:25 +01:00
Add debug log window
This commit is contained in:
parent
8b45ec4940
commit
7eb85f094f
8 changed files with 111 additions and 8 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1113,6 +1113,7 @@ dependencies = [
|
|||
name = "musicus_backend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"fragile",
|
||||
"gio",
|
||||
"glib 0.16.7",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
fragile = "2"
|
||||
gio = "0.16"
|
||||
glib = "0.16"
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
crates/musicus/res/icons/copy-symbolic.svg
Normal file
2
crates/musicus/res/icons/copy-symbolic.svg
Normal 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 |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(©_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(
|
||||
>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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue