From 50a6a1c927271eff916f8306457262392b0b4a33 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 5 Apr 2023 09:38:32 +0200 Subject: [PATCH 1/5] feat(dtmm): Colorize log output Parses ANSI codes generated by tracing/color-eyre into druid's RichText attributes. --- Cargo.lock | 100 ++++++++++++++++++++++++--- crates/dtmm/Cargo.toml | 1 + crates/dtmm/src/controller/worker.rs | 4 +- crates/dtmm/src/main.rs | 1 + crates/dtmm/src/state/data.rs | 10 +-- crates/dtmm/src/state/delegate.rs | 11 +-- crates/dtmm/src/ui/window/main.rs | 21 +++--- crates/dtmm/src/util/ansi.rs | 93 +++++++++++++++++++++++++ crates/dtmm/src/util/log.rs | 11 ++- 9 files changed, 214 insertions(+), 38 deletions(-) create mode 100644 crates/dtmm/src/util/ansi.rs diff --git a/Cargo.lock b/Cargo.lock index 1e94874..547e72e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcb2392079bf27198570d6af79ecbd9ec7d8f16d3ec6b60933922fdb66287127" +dependencies = [ + "heapless", + "nom 4.2.3", +] + [[package]] name = "anyhow" version = "1.0.69" @@ -62,6 +72,18 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.6", + "stable_deref_trait", +] + [[package]] name = "associative-cache" version = "1.0.1" @@ -191,7 +213,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.6", ] [[package]] @@ -321,7 +343,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array", + "generic-array 0.14.6", ] [[package]] @@ -614,7 +636,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.6", "typenum", ] @@ -807,6 +829,7 @@ dependencies = [ name = "dtmm" version = "0.1.0" dependencies = [ + "ansi-parser", "bitflags", "clap", "color-eyre", @@ -1272,6 +1295,24 @@ dependencies = [ "system-deps", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -1279,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -1480,12 +1521,33 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heapless" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74911a68a1658cfcfb61bc0ccfbd536e3b6e906f8c2f7883ee50157e3e2184f1" +dependencies = [ + "as-slice", + "generic-array 0.13.3", + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -1609,7 +1671,7 @@ dependencies = [ "serde", "sized-chunks", "typenum", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -2023,6 +2085,16 @@ version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + [[package]] name = "nom" version = "7.1.3" @@ -2576,7 +2648,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -2587,7 +2659,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -3091,6 +3163,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "steamid-ng" version = "1.0.0" @@ -3634,7 +3712,7 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" dependencies = [ - "version_check", + "version_check 0.9.4", ] [[package]] @@ -3768,6 +3846,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + [[package]] name = "version_check" version = "0.9.4" diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 4b651cf..18ba7b1 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -31,3 +31,4 @@ lazy_static = "1.4.0" colors-transform = "0.2.11" usvg = "0.25.0" druid-widget-nursery = "0.1" +ansi-parser = "0.8.0" diff --git a/crates/dtmm/src/controller/worker.rs b/crates/dtmm/src/controller/worker.rs index b30ca9c..4b229b5 100644 --- a/crates/dtmm/src/controller/worker.rs +++ b/crates/dtmm/src/controller/worker.rs @@ -174,7 +174,7 @@ async fn handle_action( async fn handle_log( event_sink: Arc>, - log_queue: Arc>>, + log_queue: Arc>>>, ) { while let Some(line) = log_queue.write().await.recv().await { let event_sink = event_sink.clone(); @@ -189,7 +189,7 @@ async fn handle_log( pub(crate) fn work_thread( event_sink: Arc>, action_queue: Arc>>, - log_queue: Arc>>, + log_queue: Arc>>>, ) -> Result<()> { let rt = Runtime::new()?; diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index 288f1f1..4967b6a 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -23,6 +23,7 @@ use crate::ui::theme; mod controller; mod state; mod util { + pub mod ansi; pub mod config; pub mod log; } diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs index 8bdcd92..d1eb2e2 100644 --- a/crates/dtmm/src/state/data.rs +++ b/crates/dtmm/src/state/data.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use std::sync::Arc; use druid::im::{HashMap, Vector}; +use druid::text::RichText; use druid::{Data, ImageBuf, Lens, WindowHandle, WindowId}; use dtmt_shared::ModConfig; use nexusmods::Mod as NexusMod; @@ -157,7 +158,7 @@ pub(crate) struct State { pub nexus_api_key: Arc, #[data(ignore)] - pub log: Arc, + pub log: Vector, // True, when the initial loading of configuration and mods is still in progress pub loading: bool, @@ -194,7 +195,7 @@ impl State { game_dir: Arc::new(PathBuf::new()), data_dir: Arc::new(PathBuf::new()), nexus_api_key: Arc::new(String::new()), - log: Arc::new(String::new()), + log: Vector::new(), windows: HashMap::new(), loading: true, } @@ -223,9 +224,4 @@ impl State { pub fn can_move_mod_up(&self) -> bool { self.selected_mod_index.map(|i| i > 0).unwrap_or(false) } - - pub(crate) fn add_log_line(&mut self, line: String) { - let log = Arc::make_mut(&mut self.log); - log.push_str(&line); - } } diff --git a/crates/dtmm/src/state/delegate.rs b/crates/dtmm/src/state/delegate.rs index 02c93d4..eb6af40 100644 --- a/crates/dtmm/src/state/delegate.rs +++ b/crates/dtmm/src/state/delegate.rs @@ -1,12 +1,14 @@ use std::{path::PathBuf, sync::Arc}; use color_eyre::Report; +use druid::im::Vector; use druid::{ - im::Vector, AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, - Target, WindowHandle, WindowId, + AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target, + WindowHandle, WindowId, }; use tokio::sync::mpsc::UnboundedSender; +use crate::util::ansi::ansi_to_rich_text; use crate::{ui::window, util::config::Config}; use super::{ModInfo, State}; @@ -32,7 +34,7 @@ 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"); +pub(crate) const ACTION_LOG: Selector>> = Selector::new("dtmm.action.log"); pub(crate) const ACTION_START_SAVE_SETTINGS: Selector = Selector::new("dtmm.action.start-save-settings"); @@ -252,7 +254,8 @@ impl AppDelegate for Delegate { .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); + let line = String::from_utf8_lossy(&line); + state.log.push_back(ansi_to_rich_text(&line)); } Handled::Yes } diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index 988882e..fbedb8f 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -451,17 +451,18 @@ 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) - .padding(4.) - .scroll() - .vertical() - .controller(AutoScrollController); + let list = List::new(|| { + Label::raw() + .with_font(FontDescriptor::new(FontFamily::MONOSPACE)) + .with_line_break_mode(LineBreaking::WordWrap) + }) + .lens(State::log) + .padding(4.) + .scroll() + .vertical() + .controller(AutoScrollController); - let inner = Border::new(label) + let inner = Border::new(list) .with_color(theme::COLOR_FG2) .with_top_border(1.); diff --git a/crates/dtmm/src/util/ansi.rs b/crates/dtmm/src/util/ansi.rs new file mode 100644 index 0000000..6bb8698 --- /dev/null +++ b/crates/dtmm/src/util/ansi.rs @@ -0,0 +1,93 @@ +use ansi_parser::{AnsiParser, AnsiSequence, Output}; +use druid::text::{RichText, RichTextBuilder}; +use druid::{Color, FontStyle, FontWeight}; + +use crate::ui::theme; + +#[derive(Default, Debug)] +struct TextState { + color: Option, + dim: bool, + bold: bool, + underline: bool, + strikethrough: bool, + italic: bool, +} + +pub fn ansi_to_rich_text(input: &str) -> RichText { + let mut builder = RichTextBuilder::new(); + + let mut state = TextState::default(); + + for token in input.ansi_parse() { + match token { + Output::TextBlock(text) => { + dbg!(&state); + dbg!(&text); + + let mut attr = builder.push(text); + attr.underline(state.underline); + attr.strikethrough(state.strikethrough); + + if state.bold { + attr.weight(FontWeight::BOLD); + } + + if state.italic { + attr.style(FontStyle::Italic); + } + + if let Some(color) = state.color { + attr.text_color(color); + } + } + Output::Escape(AnsiSequence::SetGraphicsMode(values)) => { + for v in values { + match v { + 0 => { + state = Default::default(); + break; + } + 1 => state.bold = true, + 2 => state.dim = true, + 3 => state.italic = true, + 4 => state.underline = true, + 9 => state.strikethrough = true, + 22 => { + state.bold = false; + state.dim = false; + } + 23 => state.italic = false, + 24 => state.underline = false, + 29 => state.underline = false, + 30..=40 | 90..=100 => { + let mut col = v - 30; + if col > 9 { + state.bold = true; + col -= 60; + } + + state.color = match col { + // This escape code is usually called 'black', but is actually used + // as "foreground color", in regards to light themes. + 1 => Some(theme::COLOR_FG), + 2 => Some(theme::COLOR_RED_LIGHT), + 3 => Some(theme::COLOR_GREEN_LIGHT), + 4 => Some(theme::COLOR_YELLOW_LIGHT), + 5 => Some(theme::COLOR_BLUE_LIGHT), + 6 => Some(theme::COLOR_PURPLE_LIGHT), + 7 => Some(theme::COLOR_AQUA_LIGHT), + 9 => None, + _ => unreachable!(), + }; + } + _ => {} + } + } + } + Output::Escape(_) => {} + } + } + + builder.build() +} diff --git a/crates/dtmm/src/util/log.rs b/crates/dtmm/src/util/log.rs index 1b379dc..97bb9a5 100644 --- a/crates/dtmm/src/util/log.rs +++ b/crates/dtmm/src/util/log.rs @@ -8,11 +8,11 @@ use tracing_subscriber::prelude::*; use tracing_subscriber::EnvFilter; pub struct ChannelWriter { - tx: UnboundedSender, + tx: UnboundedSender>, } impl ChannelWriter { - pub fn new(tx: UnboundedSender) -> Self { + pub fn new(tx: UnboundedSender>) -> Self { Self { tx } } } @@ -20,12 +20,9 @@ impl ChannelWriter { impl std::io::Write for ChannelWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { let tx = self.tx.clone(); - let stripped = strip_ansi_escapes::strip(buf)?; - let string = String::from_utf8_lossy(&stripped).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); + let _ = tx.send(buf.to_vec()); Ok(buf.len()) } @@ -35,7 +32,7 @@ impl std::io::Write for ChannelWriter { } } -pub fn create_tracing_subscriber(tx: UnboundedSender) { +pub fn create_tracing_subscriber(tx: UnboundedSender>) { let env_layer = if cfg!(debug_assertions) { EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")) } else { -- 2.45.3 From c7ec318e83fb73c66ec30405ac4c655fede0e766 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 5 Apr 2023 13:42:16 +0200 Subject: [PATCH 2/5] chore(dtmm): Remove debug logs --- crates/dtmm/src/util/ansi.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/dtmm/src/util/ansi.rs b/crates/dtmm/src/util/ansi.rs index 6bb8698..f827c7d 100644 --- a/crates/dtmm/src/util/ansi.rs +++ b/crates/dtmm/src/util/ansi.rs @@ -22,9 +22,6 @@ pub fn ansi_to_rich_text(input: &str) -> RichText { for token in input.ansi_parse() { match token { Output::TextBlock(text) => { - dbg!(&state); - dbg!(&text); - let mut attr = builder.push(text); attr.underline(state.underline); attr.strikethrough(state.strikethrough); -- 2.45.3 From c4425f5b6bfd0369e3e784b5753e01a899a538eb Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 5 Apr 2023 13:44:30 +0200 Subject: [PATCH 3/5] fix(dtmm): Trim whitespace in log lines --- crates/dtmm/src/state/delegate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dtmm/src/state/delegate.rs b/crates/dtmm/src/state/delegate.rs index eb6af40..1cf3cda 100644 --- a/crates/dtmm/src/state/delegate.rs +++ b/crates/dtmm/src/state/delegate.rs @@ -255,7 +255,7 @@ impl AppDelegate for Delegate { .expect("command type matched but didn't contain the expected value"); if let Some(line) = line.take() { let line = String::from_utf8_lossy(&line); - state.log.push_back(ansi_to_rich_text(&line)); + state.log.push_back(ansi_to_rich_text(line.trim())); } Handled::Yes } -- 2.45.3 From f30608e6f153a481dafc2c2704337489c5b3bb3c Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 5 Apr 2023 13:45:26 +0200 Subject: [PATCH 4/5] feat(dtmm): Enable colors for regular log lines --- Cargo.lock | 10 ++++++++++ crates/dtmm/src/util/log.rs | 2 -- lib/dtmt-shared/Cargo.toml | 1 + lib/dtmt-shared/src/log.rs | 26 ++++++++++++++++++++++++-- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 547e72e..ba688e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,15 @@ dependencies = [ "nom 4.2.3", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.69" @@ -897,6 +906,7 @@ dependencies = [ name = "dtmt-shared" version = "0.1.0" dependencies = [ + "ansi_term", "color-eyre", "serde", "steamlocate", diff --git a/crates/dtmm/src/util/log.rs b/crates/dtmm/src/util/log.rs index 97bb9a5..75db550 100644 --- a/crates/dtmm/src/util/log.rs +++ b/crates/dtmm/src/util/log.rs @@ -47,8 +47,6 @@ pub fn create_tracing_subscriber(tx: UnboundedSender>) { }; 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_fields)) .with_writer(move || ChannelWriter::new(tx.clone())) diff --git a/lib/dtmt-shared/Cargo.toml b/lib/dtmt-shared/Cargo.toml index eb9591d..4412266 100644 --- a/lib/dtmt-shared/Cargo.toml +++ b/lib/dtmt-shared/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +ansi_term = "0.12.1" color-eyre = "0.6.2" serde = "1.0.152" steamlocate = { path = "../../lib/steamlocate-rs", version = "*" } diff --git a/lib/dtmt-shared/src/log.rs b/lib/dtmt-shared/src/log.rs index 3c46a4b..ab0b7b5 100644 --- a/lib/dtmt-shared/src/log.rs +++ b/lib/dtmt-shared/src/log.rs @@ -1,10 +1,11 @@ use std::fmt::Result; +use ansi_term::Color; use time::format_description::FormatItem; use time::macros::format_description; use time::OffsetDateTime; use tracing::field::Field; -use tracing::{Event, Metadata, Subscriber}; +use tracing::{Event, Level, Metadata, Subscriber}; use tracing_error::ErrorLayer; use tracing_subscriber::filter::FilterFn; use tracing_subscriber::fmt::format::{debug_fn, Writer}; @@ -49,7 +50,28 @@ where 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())?; + let level = meta.level(); + // Sadly, tracing's `Level` is a struct, not an enum, so we can't properly `match` it. + let color = if *level == Level::TRACE { + Color::Purple + } else if *level == Level::DEBUG { + Color::Blue + } else if *level == Level::INFO { + Color::Green + } else if *level == Level::WARN { + Color::Yellow + } else if *level == Level::ERROR { + Color::Red + } else { + unreachable!() + }; + + write!( + writer, + "[{}] [{:>5}] ", + time, + color.bold().paint(format!("{}", level)) + )?; ctx.field_format().format_fields(writer.by_ref(), event)?; -- 2.45.3 From 01b1428b3812db1d9ddcc08f7863009007cfb338 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 5 Apr 2023 14:48:38 +0200 Subject: [PATCH 5/5] fix(dtmm): Fix updating log view --- crates/dtmm/src/state/data.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs index d1eb2e2..b1adcb6 100644 --- a/crates/dtmm/src/state/data.rs +++ b/crates/dtmm/src/state/data.rs @@ -156,8 +156,6 @@ pub(crate) struct State { pub game_dir: Arc, pub data_dir: Arc, pub nexus_api_key: Arc, - - #[data(ignore)] pub log: Vector, // True, when the initial loading of configuration and mods is still in progress pub loading: bool, -- 2.45.3