diff --git a/src/core/logging.rs b/src/core/logging.rs index e2275be..9af3290 100644 --- a/src/core/logging.rs +++ b/src/core/logging.rs @@ -1,6 +1,7 @@ use std::collections::{hash_map::Entry, HashMap}; +use std::fmt::Write as FmtWrite; use std::fs::File; -use std::io::Write; +use std::io::Write as IOWrite; use std::path::{Path, PathBuf}; use steel_core::chat::Message; @@ -107,51 +108,9 @@ impl ChatLoggerBackend { } fn chat_path(&self, chat_name: &str) -> PathBuf { - self.log_directory.join(chat_name.to_lowercase()).with_extension("log") - } - - fn format_message(log_line_format: &str, message: &Message) -> String { - let mut result = String::new(); - let mut placeholder = String::new(); - let mut in_placeholder = false; - - for c in log_line_format.chars() { - match c { - '{' => { - in_placeholder = true; - placeholder.clear(); - } - '}' => { - if in_placeholder { - result.push_str(&Self::resolve_placeholder(&placeholder, message)); - in_placeholder = false; - } else { - result.push(c); - } - } - _ => { - if in_placeholder { - placeholder.push(c); - } else { - result.push(c); - } - } - } - } - - result - } - - fn resolve_placeholder(placeholder: &str, message: &Message) -> String { - if let Some(format) = placeholder.strip_prefix("date:") { - message.time.format(format).to_string() - } else { - match placeholder { - "username" => message.username.clone(), - "text" => message.text.clone(), - _ => String::from("{unknown}"), - } - } + self.log_directory + .join(chat_name.to_lowercase()) + .with_extension("log") } fn log(&mut self, chat_name: String, message: Message) -> std::io::Result<()> { @@ -188,7 +147,7 @@ impl ChatLoggerBackend { } }; - let formatted_message = Self::format_message(&self.log_line_format, &message); + let formatted_message = format_message_for_logging(&self.log_line_format, &message); if let Err(e) = writeln!(&mut f, "{}", formatted_message) { log::error!("Failed to append a chat log line for {}: {}", chat_name, e); return Err(e); @@ -204,3 +163,51 @@ impl ChatLoggerBackend { } } } + +pub fn format_message_for_logging(log_line_format: &str, message: &Message) -> String { + let mut result = String::new(); + let mut placeholder = String::new(); + let mut in_placeholder = false; + + for c in log_line_format.chars() { + match c { + '{' => { + in_placeholder = true; + placeholder.clear(); + } + '}' => { + if in_placeholder { + result.push_str(&resolve_placeholder(&placeholder, message)); + in_placeholder = false; + } else { + result.push(c); + } + } + _ => { + if in_placeholder { + placeholder.push(c); + } else { + result.push(c); + } + } + } + } + + result +} + +fn resolve_placeholder(placeholder: &str, message: &Message) -> String { + if let Some(date_format) = placeholder.strip_prefix("date:") { + let mut buf = String::new(); + match write!(&mut buf, "{}", message.time.format(date_format)) { + Ok(_) => buf, + Err(_) => format!("{{date:{}}}", date_format), + } + } else { + match placeholder { + "username" => message.username.clone(), + "text" => message.text.clone(), + _ => String::from("{unknown}"), + } + } +} diff --git a/src/core/os.rs b/src/core/os.rs index d5363ee..36dcc9f 100644 --- a/src/core/os.rs +++ b/src/core/os.rs @@ -1,42 +1,65 @@ +use std::path::Path; + #[derive(Debug)] -enum OpenTarget<'opener> { - AppFile(&'opener str), - AppDirectory, +enum RuntimeDirectoryPath<'opener> { + File(&'opener str), + RootDirectory, + Subdirectory(&'opener str), +} + +fn open_fs_path_in_explorer(path: &Path, target: &str) { + if let Some(path) = path.to_str() { + let path = path.to_owned(); + let (executable, args) = if cfg!(target_os = "windows") { + // let file_arg = format!("/select,{}", log_path); + ("explorer.exe", vec![path]) + } else if cfg!(target_os = "macos") { + ("open", vec![path]) + } else { + ("xdg-open", vec![path]) + }; + if let Err(e) = std::process::Command::new(executable).args(&args).spawn() { + log::error!( + "failed to open {target:?} from UI: {e:?} (command line: \"{executable} {args:?})" + ); + } + } } -fn open_app_path(target: OpenTarget) { +fn open_app_path(target: RuntimeDirectoryPath) { if let Ok(mut path) = std::env::current_exe() { match target { - OpenTarget::AppFile(file_name) => path.set_file_name(file_name), - OpenTarget::AppDirectory => { + RuntimeDirectoryPath::File(file_name) => path.set_file_name(file_name), + RuntimeDirectoryPath::RootDirectory => { path = path.parent().unwrap().to_path_buf(); } - } - if let Some(path) = path.to_str() { - let path = path.to_owned(); - let (executable, args) = if cfg!(target_os = "windows") { - // let file_arg = format!("/select,{}", log_path); - ("explorer.exe", vec![path]) - } else if cfg!(target_os = "macos") { - ("open", vec![path]) - } else { - ("xdg-open", vec![path]) - }; - if let Err(e) = std::process::Command::new(executable).args(&args).spawn() { - log::error!("failed to open {target:?} from UI: {e:?} (command line: \"{executable} {args:?})"); + RuntimeDirectoryPath::Subdirectory(subdir) => { + path = path.parent().unwrap().to_path_buf().join(subdir) } } + open_fs_path_in_explorer(&path, &format!("{:?}", target)); } } pub fn open_runtime_log() { - open_app_path(OpenTarget::AppFile("runtime.log")) + open_app_path(RuntimeDirectoryPath::File("runtime.log")) } + pub fn open_settings_file() { - open_app_path(OpenTarget::AppFile("settings.yaml")) + open_app_path(RuntimeDirectoryPath::File("settings.yaml")) } + pub fn open_own_directory() { - open_app_path(OpenTarget::AppDirectory) + open_app_path(RuntimeDirectoryPath::RootDirectory) +} + +pub fn open_external_directory(path: &str) { + let d = std::path::Path::new(path); + if d.is_relative() { + open_app_path(RuntimeDirectoryPath::Subdirectory(path)); + } else { + open_fs_path_in_explorer(d, path); + } } pub fn cleanup_after_update() { diff --git a/src/gui/settings/journal.rs b/src/gui/settings/journal.rs new file mode 100644 index 0000000..ac7700a --- /dev/null +++ b/src/gui/settings/journal.rs @@ -0,0 +1,83 @@ +use eframe::egui::RichText; +use steel_core::chat; + +use super::SettingsWindow; +use crate::{ + core::{self, logging::format_message_for_logging}, + gui::state::UIState, +}; + +impl SettingsWindow { + pub(super) fn show_logging_tab(&mut self, ui: &mut eframe::egui::Ui, state: &mut UIState) { + ui.vertical(|ui| { + ui.heading("chat logging"); + ui.checkbox( + &mut state.settings.journal.chat_events.enabled, + "enable chat logging", + ); + + ui.horizontal(|ui| { + ui.label("logs directory"); + ui.text_edit_singleline(&mut state.settings.journal.chat_events.directory) + .on_hover_text_at_pointer("location of all the log files"); + + // TODO(logging): Test for presence and refuse to open if it doesn't exist. + if ui.button("open").clicked() { + core::os::open_external_directory( + &mut state.settings.journal.chat_events.directory, + ); + } + }); + + ui.label("format of a single line"); + ui.text_edit_multiline(&mut state.settings.journal.chat_events.format); + + ui.horizontal_wrapped(|ui| { + ui.label(RichText::new("preview: →").color(ui.visuals().warn_fg_color)); + let message = + chat::Message::new_text("WilliamGibson", "I think I left my cyberdeck on"); + let formatted_message = format_message_for_logging( + &state.settings.journal.chat_events.format, + &message, + ); + ui.label(formatted_message); + ui.label(RichText::new("←").color(ui.visuals().warn_fg_color)); + }); + + ui.collapsing("click to show help", |ui| { + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + + ui.label("allowed placeholders:\n"); + + ui.label("- "); + ui.label(RichText::new("{username}").color(ui.style().visuals.warn_fg_color)); + ui.label(" - author of the message\n"); + + ui.label("- "); + ui.label(RichText::new("{text}").color(ui.style().visuals.warn_fg_color)); + ui.label(" - message text\n"); + + ui.label("- "); + ui.label(RichText::new("{date:").color(ui.style().visuals.warn_fg_color)); + ui.label(RichText::new("dateformat").color(ui.style().visuals.error_fg_color)); + ui.label(RichText::new("}").color(ui.style().visuals.warn_fg_color)); + ui.label(" - message date/time, where "); + ui.label(RichText::new("dateformat").color(ui.style().visuals.error_fg_color)); + ui.label(" is replaced by a format string. example: "); + + ui.label(RichText::new("{date:").color(ui.style().visuals.warn_fg_color)); + ui.label( + RichText::new("%Y-%m-%d %H:%M:%S").color(ui.style().visuals.error_fg_color), + ); + ui.label(RichText::new("}").color(ui.style().visuals.warn_fg_color)); + ui.label(" ("); + ui.hyperlink_to("click for more examples", "https://strftime.net"); + ui.label(")"); + }); + }); + }); + + // TODO(logging): Add a setting for logging system events. + } +} diff --git a/src/gui/settings/mod.rs b/src/gui/settings/mod.rs index dbdb0d5..51f6f13 100644 --- a/src/gui/settings/mod.rs +++ b/src/gui/settings/mod.rs @@ -2,6 +2,7 @@ mod application; mod chat; +mod journal; mod notifications; mod ui; @@ -22,6 +23,7 @@ pub enum Tab { Notifications, #[cfg(feature = "glass")] Moderation, + Logging, } #[derive(Default)] @@ -60,6 +62,8 @@ impl SettingsWindow { #[cfg(feature = "glass")] ui.selectable_value(&mut self.active_tab, Tab::Moderation, "moderation"); + + ui.selectable_value(&mut self.active_tab, Tab::Logging, "logging"); }); ui.separator(); @@ -72,6 +76,8 @@ impl SettingsWindow { #[cfg(feature = "glass")] Tab::Moderation => state.glass.show_ui(ui, &state.settings.ui.theme), + + Tab::Logging => self.show_logging_tab(ui, state), } });