diff --git a/Cargo.lock b/Cargo.lock index db80a10..6439337 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,9 +2293,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ "itoa", "libc", @@ -2313,9 +2313,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "time-macros" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" dependencies = [ "time-core", ] diff --git a/crates/dtmm/src/log.rs b/crates/dtmm/src/log.rs new file mode 100644 index 0000000..12aeeff --- /dev/null +++ b/crates/dtmm/src/log.rs @@ -0,0 +1,80 @@ +use tokio::sync::mpsc::UnboundedSender; +use tracing_error::ErrorLayer; +use tracing_subscriber::filter::FilterFn; +use tracing_subscriber::fmt; +use tracing_subscriber::fmt::format::debug_fn; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::prelude::*; +use tracing_subscriber::EnvFilter; + +// I currently cannot find a way to add a parameter to `dtmt_shared::create_tracing_subscriber` +// that would allow me to pass an extra `Layer` to that function. So, for now, +// its code has to be duplicated here. + +pub struct ChannelWriter { + tx: UnboundedSender, +} + +impl ChannelWriter { + pub fn new(tx: UnboundedSender) -> Self { + Self { tx } + } +} + +impl std::io::Write for ChannelWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let tx = self.tx.clone(); + let string = String::from_utf8_lossy(buf).to_string(); + + // The `send` errors when the receiving end has closed. + // But there's not much we can do at that point, so we just ignore it. + let _ = tx.send(string); + + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +pub fn create_tracing_subscriber(tx: UnboundedSender) { + let env_layer = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); + + let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) { + let fmt_layer = fmt::layer().pretty(); + (Some(fmt_layer), None, None) + } else { + // Creates a layer that + // - only prints events that contain a message + // - does not print fields + // - does not print spans/targets + // - only prints time, not date + let fmt_layer = fmt::layer() + .event_format(dtmt_shared::Formatter) + .fmt_fields(debug_fn(dtmt_shared::format_field)); + + ( + None, + Some(fmt_layer), + Some(FilterFn::new(dtmt_shared::filter)), + ) + }; + + let channel_layer = fmt::layer() + // TODO: Re-enable and implement a formatter for the Druid widget + .with_ansi(false) + .event_format(dtmt_shared::Formatter) + .fmt_fields(debug_fn(dtmt_shared::format_field)) + .with_writer(move || ChannelWriter::new(tx.clone())); + + tracing_subscriber::registry() + .with(channel_layer) + .with(filter_layer) + .with(env_layer) + .with(dev_stdout_layer) + .with(prod_stdout_layer) + .with(ErrorLayer::new(fmt::format::Pretty::default())) + .init(); +} diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index 352aeb0..6a3252a 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -1,170 +1,35 @@ #![recursion_limit = "256"] #![feature(let_chains)] -use std::fs; -use std::io::ErrorKind; use std::path::PathBuf; use std::sync::Arc; use clap::command; -use clap::parser::ValueSource; use clap::value_parser; use clap::Arg; use color_eyre::eyre::Context; -use color_eyre::Report; -use color_eyre::Result; +use color_eyre::{Report, Result}; use druid::AppLauncher; -use druid::ExtEventSink; -use druid::SingleUse; -use druid::Target; -use engine::delete_mod; -use engine::import_mod; -use engine::reset_mod_deployment; -use serde::Deserialize; -use serde::Serialize; -use state::ACTION_FINISH_ADD_MOD; -use state::ACTION_FINISH_DELETE_SELECTED_MOD; -use state::ACTION_FINISH_RESET_DEPLOYMENT; -use tokio::runtime::Runtime; -use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::RwLock; -use crate::engine::deploy_mods; -use crate::state::{AsyncAction, Delegate, State, ACTION_FINISH_DEPLOY}; +use crate::state::{Delegate, State}; +use crate::worker::work_thread; mod controller; mod engine; +mod log; mod main_window; mod state; mod theme; +mod util; mod widget; - -#[derive(Clone, Debug, Serialize, Deserialize)] -struct Config { - data_dir: Option, - game_dir: Option, -} - -fn work_thread( - event_sink: Arc>, - action_queue: Arc>>, -) -> Result<()> { - let rt = Runtime::new()?; - - rt.block_on(async { - while let Some(action) = action_queue.write().await.recv().await { - let event_sink = event_sink.clone(); - match action { - AsyncAction::DeployMods(state) => tokio::spawn(async move { - if let Err(err) = deploy_mods(state).await { - tracing::error!("Failed to deploy mods: {:?}", err); - } - - event_sink - .write() - .await - .submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto) - .expect("failed to send command"); - }), - AsyncAction::AddMod((state, info)) => tokio::spawn(async move { - match import_mod(state, info).await { - Ok(mod_info) => { - event_sink - .write() - .await - .submit_command( - ACTION_FINISH_ADD_MOD, - SingleUse::new(mod_info), - Target::Auto, - ) - .expect("failed to send command"); - } - Err(err) => { - tracing::error!("Failed to import mod: {:?}", err); - } - } - }), - AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move { - if let Err(err) = delete_mod(state, &info).await { - tracing::error!( - "Failed to delete mod files. \ - You might want to clean up the data directory manually. \ - Reason: {:?}", - err - ); - } - - event_sink - .write() - .await - .submit_command( - ACTION_FINISH_DELETE_SELECTED_MOD, - SingleUse::new(info), - Target::Auto, - ) - .expect("failed to send command"); - }), - AsyncAction::ResetDeployment(state) => tokio::spawn(async move { - if let Err(err) = reset_mod_deployment(state).await { - tracing::error!("Failed to reset mod deployment: {:?}", err); - } - - event_sink - .write() - .await - .submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto) - .expect("failed to send command"); - }), - }; - } - }); - - Ok(()) -} - -#[cfg(not(arget_os = "windows"))] -fn get_default_config_path() -> PathBuf { - let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| { - let home = std::env::var("HOME").unwrap_or_else(|_| { - let user = std::env::var("USER").expect("user env variable not set"); - format!("/home/{user}") - }); - format!("{home}/.config") - }); - - PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg") -} - -#[cfg(target_os = "windows")] -fn get_default_config_path() -> PathBuf { - let config_dir = std::env::var("APPDATA").expect("appdata env var not set"); - PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg") -} - -#[cfg(not(arget_os = "windows"))] -fn get_default_data_dir() -> PathBuf { - let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| { - let home = std::env::var("HOME").unwrap_or_else(|_| { - let user = std::env::var("USER").expect("user env variable not set"); - format!("/home/{user}") - }); - format!("{home}/.local/share") - }); - - PathBuf::from(data_dir).join("dtmm") -} - -#[cfg(target_os = "windows")] -fn get_default_data_dir() -> PathBuf { - let data_dir = std::env::var("APPDATA").expect("appdata env var not set"); - PathBuf::from(data_dir).join("dtmm") -} +mod worker; #[tracing::instrument] fn main() -> Result<()> { color_eyre::install()?; - let default_config_path = get_default_config_path(); + let default_config_path = util::get_default_config_path(); tracing::trace!(default_config_path = %default_config_path.display()); @@ -185,78 +50,30 @@ fn main() -> Result<()> { ) .get_matches(); - dtmt_shared::create_tracing_subscriber(); + let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel(); + log::create_tracing_subscriber(log_tx); unsafe { oodle_sys::init(matches.get_one::("oodle")); } - let config: Config = { - let path = matches - .get_one::("config") - .expect("argument missing despite default"); - match fs::read(path) { - Ok(data) => { - let data = String::from_utf8(data).wrap_err_with(|| { - format!("config file {} contains invalid UTF-8", path.display()) - })?; - serde_sjson::from_str(&data) - .wrap_err_with(|| format!("invalid config file {}", path.display()))? - } - Err(err) if err.kind() == ErrorKind::NotFound => { - if matches.value_source("config") != Some(ValueSource::DefaultValue) { - return Err(err).wrap_err_with(|| { - format!("failed to read config file {}", path.display()) - })?; - } - - { - let parent = default_config_path - .parent() - .expect("a file path always has a parent directory"); - fs::create_dir_all(parent).wrap_err_with(|| { - format!("failed to create directories {}", parent.display()) - })?; - } - - let config = Config { - data_dir: Some(get_default_data_dir()), - game_dir: None, - }; - - { - let data = serde_sjson::to_string(&config) - .wrap_err("failed to serialize default config value")?; - fs::write(&default_config_path, data).wrap_err_with(|| { - format!( - "failed to write default config to {}", - default_config_path.display() - ) - })?; - } - - config - } - Err(err) => { - return Err(err) - .wrap_err_with(|| format!("failed to read config file {}", path.display()))?; - } - } - }; + let config = + util::read_config(&default_config_path, &matches).wrap_err("failed to read config file")?; let initial_state = State::new(config); - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - let delegate = Delegate::new(sender); + let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); + let delegate = Delegate::new(action_tx); let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate); let event_sink = launcher.get_external_handle(); std::thread::spawn(move || { let event_sink = Arc::new(RwLock::new(event_sink)); - let receiver = Arc::new(RwLock::new(receiver)); + let action_rx = Arc::new(RwLock::new(action_rx)); + let log_rx = Arc::new(RwLock::new(log_rx)); loop { - if let Err(err) = work_thread(event_sink.clone(), receiver.clone()) { + if let Err(err) = work_thread(event_sink.clone(), action_rx.clone(), log_rx.clone()) { tracing::error!("Work thread failed, restarting: {:?}", err); } } diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index 1b06c95..6fcbc87 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -1,10 +1,11 @@ use druid::im::Vector; use druid::widget::{ - Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split, - TextBox, ViewSwitcher, + Align, Button, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Maybe, + Scroll, SizedBox, Split, TextBox, ViewSwitcher, }; use druid::{ - lens, FileDialogOptions, FileSpec, Insets, LensExt, SingleUse, Widget, WidgetExt, WindowDesc, + lens, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, LensExt, SingleUse, + Widget, WidgetExt, WindowDesc, }; use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT}; @@ -261,9 +262,25 @@ fn build_main() -> impl Widget { ) } +fn build_log_view() -> impl Widget { + let font = FontDescriptor::new(FontFamily::MONOSPACE); + let label = Label::raw() + .with_font(font) + .with_line_break_mode(LineBreaking::WordWrap) + .lens(State::log); + + SizedBox::new(label) + .expand_width() + .height(128.0) + .scroll() + .vertical() +} + fn build_window() -> impl Widget { + // TODO: Add borders between the sections Flex::column() .must_fill_main_axis(true) .with_child(build_top_bar()) .with_flex_child(build_main(), 1.0) + .with_child(build_log_view()) } diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 76f1c8a..71f66c2 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -10,7 +10,7 @@ use druid::{ use dtmt_shared::ModConfig; use tokio::sync::mpsc::UnboundedSender; -use crate::Config; +use crate::util::Config; pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); @@ -33,6 +33,8 @@ pub(crate) const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action pub(crate) const ACTION_FINISH_ADD_MOD: Selector> = Selector::new("dtmm.action.finish-add-mod"); +pub(crate) const ACTION_LOG: Selector> = Selector::new("dtmm.action.log"); + #[derive(Copy, Clone, Data, Debug, PartialEq)] pub(crate) enum View { Mods, @@ -154,6 +156,7 @@ pub(crate) struct State { game_dir: Arc, data_dir: Arc, ctx: Arc, + log: Arc, } impl State { @@ -170,8 +173,9 @@ impl State { selected_mod_index: None, is_deployment_in_progress: false, is_reset_in_progress: false, - game_dir: Arc::new(config.game_dir.unwrap_or_default()), - data_dir: Arc::new(config.data_dir.unwrap_or_default()), + game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()), + data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()), + log: Arc::new(String::new()), } } @@ -230,6 +234,11 @@ impl State { pub(crate) fn get_ctx(&self) -> Arc { self.ctx.clone() } + + pub(crate) fn add_log_line(&mut self, line: String) { + let log = Arc::make_mut(&mut self.log); + log.push_str(&line); + } } pub(crate) struct SelectedModLens; @@ -454,6 +463,15 @@ impl AppDelegate for Delegate { } Handled::Yes } + cmd if cmd.is(ACTION_LOG) => { + let line = cmd + .get(ACTION_LOG) + .expect("command type matched but didn't contain the expected value"); + if let Some(line) = line.take() { + state.add_log_line(line); + } + Handled::Yes + } cmd => { if cfg!(debug_assertions) { tracing::warn!("Unknown command: {:?}", cmd); diff --git a/crates/dtmm/src/util.rs b/crates/dtmm/src/util.rs new file mode 100644 index 0000000..4483c53 --- /dev/null +++ b/crates/dtmm/src/util.rs @@ -0,0 +1,117 @@ +use std::fs; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; + +use clap::{parser::ValueSource, ArgMatches}; +use color_eyre::{eyre::Context, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct Config { + data_dir: Option, + game_dir: Option, +} + +impl Config { + pub fn game_dir(&self) -> Option<&PathBuf> { + self.game_dir.as_ref() + } + + pub fn data_dir(&self) -> Option<&PathBuf> { + self.data_dir.as_ref() + } +} + +#[cfg(not(arget_os = "windows"))] +pub fn get_default_config_path() -> PathBuf { + let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| { + let user = std::env::var("USER").expect("user env variable not set"); + format!("/home/{user}") + }); + format!("{home}/.config") + }); + + PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg") +} + +#[cfg(target_os = "windows")] +pub fn get_default_config_path() -> PathBuf { + let config_dir = std::env::var("APPDATA").expect("appdata env var not set"); + PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg") +} + +#[cfg(not(arget_os = "windows"))] +pub fn get_default_data_dir() -> PathBuf { + let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| { + let user = std::env::var("USER").expect("user env variable not set"); + format!("/home/{user}") + }); + format!("{home}/.local/share") + }); + + PathBuf::from(data_dir).join("dtmm") +} + +#[cfg(target_os = "windows")] +pub fn get_default_data_dir() -> PathBuf { + let data_dir = std::env::var("APPDATA").expect("appdata env var not set"); + PathBuf::from(data_dir).join("dtmm") +} + +pub(crate) fn read_config>( + default_config_path: P, + matches: &ArgMatches, +) -> Result { + let path = matches + .get_one::("config") + .expect("argument missing despite default"); + let default_config_path = default_config_path.as_ref(); + + match fs::read(path) { + Ok(data) => { + let data = String::from_utf8(data).wrap_err_with(|| { + format!("config file {} contains invalid UTF-8", path.display()) + })?; + serde_sjson::from_str(&data) + .wrap_err_with(|| format!("invalid config file {}", path.display())) + } + Err(err) if err.kind() == ErrorKind::NotFound => { + if matches.value_source("config") != Some(ValueSource::DefaultValue) { + return Err(err) + .wrap_err_with(|| format!("failed to read config file {}", path.display()))?; + } + + { + let parent = default_config_path + .parent() + .expect("a file path always has a parent directory"); + fs::create_dir_all(parent).wrap_err_with(|| { + format!("failed to create directories {}", parent.display()) + })?; + } + + let config = Config { + data_dir: Some(get_default_data_dir()), + game_dir: None, + }; + + { + let data = serde_sjson::to_string(&config) + .wrap_err("failed to serialize default config value")?; + fs::write(default_config_path, data).wrap_err_with(|| { + format!( + "failed to write default config to {}", + default_config_path.display() + ) + })?; + } + + Ok(config) + } + Err(err) => { + Err(err).wrap_err_with(|| format!("failed to read config file {}", path.display())) + } + } +} diff --git a/crates/dtmm/src/worker.rs b/crates/dtmm/src/worker.rs new file mode 100644 index 0000000..4715480 --- /dev/null +++ b/crates/dtmm/src/worker.rs @@ -0,0 +1,114 @@ +use std::sync::Arc; + +use color_eyre::Result; +use druid::{ExtEventSink, SingleUse, Target}; +use tokio::runtime::Runtime; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::RwLock; + +use crate::engine::*; +use crate::state::*; + +async fn handle_action( + event_sink: Arc>, + action_queue: Arc>>, +) { + while let Some(action) = action_queue.write().await.recv().await { + let event_sink = event_sink.clone(); + match action { + AsyncAction::DeployMods(state) => tokio::spawn(async move { + if let Err(err) = deploy_mods(state).await { + tracing::error!("Failed to deploy mods: {:?}", err); + } + + event_sink + .write() + .await + .submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto) + .expect("failed to send command"); + }), + AsyncAction::AddMod((state, info)) => tokio::spawn(async move { + match import_mod(state, info).await { + Ok(mod_info) => { + event_sink + .write() + .await + .submit_command( + ACTION_FINISH_ADD_MOD, + SingleUse::new(mod_info), + Target::Auto, + ) + .expect("failed to send command"); + } + Err(err) => { + tracing::error!("Failed to import mod: {:?}", err); + } + } + }), + AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move { + if let Err(err) = delete_mod(state, &info).await { + tracing::error!( + "Failed to delete mod files. \ + You might want to clean up the data directory manually. \ + Reason: {:?}", + err + ); + } + + event_sink + .write() + .await + .submit_command( + ACTION_FINISH_DELETE_SELECTED_MOD, + SingleUse::new(info), + Target::Auto, + ) + .expect("failed to send command"); + }), + AsyncAction::ResetDeployment(state) => tokio::spawn(async move { + if let Err(err) = reset_mod_deployment(state).await { + tracing::error!("Failed to reset mod deployment: {:?}", err); + } + + event_sink + .write() + .await + .submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto) + .expect("failed to send command"); + }), + }; + } +} + +async fn handle_log( + event_sink: Arc>, + log_queue: Arc>>, +) { + while let Some(line) = log_queue.write().await.recv().await { + let event_sink = event_sink.clone(); + event_sink + .write() + .await + .submit_command(ACTION_LOG, SingleUse::new(line), Target::Auto) + .expect("failed to send command"); + } +} + +pub(crate) fn work_thread( + event_sink: Arc>, + action_queue: Arc>>, + log_queue: Arc>>, +) -> Result<()> { + let rt = Runtime::new()?; + + rt.block_on(async { + loop { + tokio::select! { + _ = handle_action(event_sink.clone(), action_queue.clone()) => {}, + _ = handle_log(event_sink.clone(), log_queue.clone()) => {}, + } + } + }); + + Ok(()) +} diff --git a/lib/dtmt-shared/src/log.rs b/lib/dtmt-shared/src/log.rs index 41b015d..15a26b1 100644 --- a/lib/dtmt-shared/src/log.rs +++ b/lib/dtmt-shared/src/log.rs @@ -1,113 +1,87 @@ -// Rust Analyzer cannot properly determine that `cfg!(debug_assertions)` alone does not make code -// unused. These sections should be small enough that no truly dead code slips in. +use std::fmt::Result; -#[allow(dead_code)] -mod prod { - use std::fmt::Result; +use time::format_description::FormatItem; +use time::macros::format_description; +use time::OffsetDateTime; +use tracing::field::Field; +use tracing::{Event, Metadata, Subscriber}; +use tracing_error::ErrorLayer; +use tracing_subscriber::filter::FilterFn; +use tracing_subscriber::fmt::format::{debug_fn, Writer}; +use tracing_subscriber::fmt::{self, FmtContext, FormatEvent, FormatFields}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::prelude::*; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::EnvFilter; - use time::format_description::FormatItem; - use time::macros::format_description; - use time::OffsetDateTime; - use tracing::field::Field; - use tracing::{Event, Metadata, Subscriber}; - use tracing_error::ErrorLayer; - use tracing_subscriber::filter::FilterFn; - use tracing_subscriber::fmt::format::{debug_fn, Writer}; - use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; - use tracing_subscriber::prelude::*; - use tracing_subscriber::registry::LookupSpan; - use tracing_subscriber::EnvFilter; +pub const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]"); - const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]"); - - fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result { - if field.name() == "message" { - write!(w, "{:?}", val) - } else { - Ok(()) - } +pub fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result { + if field.name() == "message" { + write!(w, "{:?}", val) + } else { + Ok(()) } +} - fn filter(metadata: &Metadata<'_>) -> bool { - metadata - .fields() - .iter() - .any(|field| field.name() == "message") +pub fn filter(metadata: &Metadata<'_>) -> bool { + metadata + .fields() + .iter() + .any(|field| field.name() == "message") +} + +pub struct Formatter; + +impl FormatEvent for Formatter +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> Result { + let meta = event.metadata(); + + let time = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + let time = time.format(TIME_FORMAT).map_err(|_| std::fmt::Error)?; + + write!(writer, "[{}] [{:>5}] ", time, meta.level())?; + + ctx.field_format().format_fields(writer.by_ref(), event)?; + + writeln!(writer) } +} - struct Formatter; +pub fn create_tracing_subscriber() { + let env_layer = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); - impl FormatEvent for Formatter - where - S: Subscriber + for<'a> LookupSpan<'a>, - N: for<'a> FormatFields<'a> + 'static, - { - fn format_event( - &self, - ctx: &FmtContext<'_, S, N>, - mut writer: Writer<'_>, - event: &Event<'_>, - ) -> Result { - let meta = event.metadata(); - - let time = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); - let time = time.format(TIME_FORMAT).map_err(|_| std::fmt::Error)?; - - write!(writer, "[{}] [{:>5}] ", time, meta.level())?; - - ctx.field_format().format_fields(writer.by_ref(), event)?; - - writeln!(writer) - } - } - - /// Creates a subscriber that - /// - only prints events that contain a message - /// - does not print fields - /// - does not print spans/targets - /// - only prints time, not date - pub fn create_tracing_subscriber() { - let filter_layer = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); - - let fmt_layer = tracing_subscriber::fmt::layer() + let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) { + let fmt_layer = fmt::layer().pretty(); + (Some(fmt_layer), None, None) + } else { + // Creates a layer that + // - only prints events that contain a message + // - does not print fields + // - does not print spans/targets + // - only prints time, not date + let fmt_layer = fmt::layer() .event_format(Formatter) .fmt_fields(debug_fn(format_field)); - tracing_subscriber::registry() - .with(FilterFn::new(filter)) - .with(filter_layer) - .with(fmt_layer) - .with(ErrorLayer::new( - tracing_subscriber::fmt::format::Pretty::default(), - )) - .init(); - } + (None, Some(fmt_layer), Some(FilterFn::new(filter))) + }; + + tracing_subscriber::registry() + .with(filter_layer) + .with(env_layer) + .with(dev_stdout_layer) + .with(prod_stdout_layer) + .with(ErrorLayer::new(fmt::format::Pretty::default())) + .init(); } - -#[allow(dead_code)] -mod dev { - use tracing_error::ErrorLayer; - use tracing_subscriber::prelude::*; - use tracing_subscriber::EnvFilter; - - pub fn create_tracing_subscriber() { - let filter_layer = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); - let fmt_layer = tracing_subscriber::fmt::layer().pretty(); - - tracing_subscriber::registry() - .with(filter_layer) - .with(fmt_layer) - .with(ErrorLayer::new( - tracing_subscriber::fmt::format::Pretty::default(), - )) - .init(); - } -} - -#[cfg(debug_assertions)] -pub use dev::create_tracing_subscriber; - -#[cfg(not(debug_assertions))] -pub use prod::create_tracing_subscriber;