feat(dtmm): Colorize log output

Parses ANSI codes generated by tracing/color-eyre into druid's RichText
attributes.
This commit is contained in:
Lucas Schwiderski 2023-04-05 09:38:32 +02:00
parent 5302eb6200
commit 50a6a1c927
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
9 changed files with 214 additions and 38 deletions

100
Cargo.lock generated
View file

@ -38,6 +38,16 @@ dependencies = [
"memchr", "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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.69" version = "1.0.69"
@ -62,6 +72,18 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" 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]] [[package]]
name = "associative-cache" name = "associative-cache"
version = "1.0.1" version = "1.0.1"
@ -191,7 +213,7 @@ version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.6",
] ]
[[package]] [[package]]
@ -321,7 +343,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.6",
] ]
[[package]] [[package]]
@ -614,7 +636,7 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.6",
"typenum", "typenum",
] ]
@ -807,6 +829,7 @@ dependencies = [
name = "dtmm" name = "dtmm"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ansi-parser",
"bitflags", "bitflags",
"clap", "clap",
"color-eyre", "color-eyre",
@ -1272,6 +1295,24 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.6" version = "0.14.6"
@ -1279,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check 0.9.4",
] ]
[[package]] [[package]]
@ -1480,12 +1521,33 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "hash32"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 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]] [[package]]
name = "heck" name = "heck"
version = "0.4.1" version = "0.4.1"
@ -1609,7 +1671,7 @@ dependencies = [
"serde", "serde",
"sized-chunks", "sized-chunks",
"typenum", "typenum",
"version_check", "version_check 0.9.4",
] ]
[[package]] [[package]]
@ -2023,6 +2085,16 @@ version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" 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]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -2576,7 +2648,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
"version_check", "version_check 0.9.4",
] ]
[[package]] [[package]]
@ -2587,7 +2659,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"version_check", "version_check 0.9.4",
] ]
[[package]] [[package]]
@ -3091,6 +3163,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "steamid-ng" name = "steamid-ng"
version = "1.0.0" version = "1.0.0"
@ -3634,7 +3712,7 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [ dependencies = [
"version_check", "version_check 0.9.4",
] ]
[[package]] [[package]]
@ -3768,6 +3846,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
[[package]]
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"

View file

@ -31,3 +31,4 @@ lazy_static = "1.4.0"
colors-transform = "0.2.11" colors-transform = "0.2.11"
usvg = "0.25.0" usvg = "0.25.0"
druid-widget-nursery = "0.1" druid-widget-nursery = "0.1"
ansi-parser = "0.8.0"

View file

@ -174,7 +174,7 @@ async fn handle_action(
async fn handle_log( async fn handle_log(
event_sink: Arc<RwLock<ExtEventSink>>, event_sink: Arc<RwLock<ExtEventSink>>,
log_queue: Arc<RwLock<UnboundedReceiver<String>>>, log_queue: Arc<RwLock<UnboundedReceiver<Vec<u8>>>>,
) { ) {
while let Some(line) = log_queue.write().await.recv().await { while let Some(line) = log_queue.write().await.recv().await {
let event_sink = event_sink.clone(); let event_sink = event_sink.clone();
@ -189,7 +189,7 @@ async fn handle_log(
pub(crate) fn work_thread( pub(crate) fn work_thread(
event_sink: Arc<RwLock<ExtEventSink>>, event_sink: Arc<RwLock<ExtEventSink>>,
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>, action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
log_queue: Arc<RwLock<UnboundedReceiver<String>>>, log_queue: Arc<RwLock<UnboundedReceiver<Vec<u8>>>>,
) -> Result<()> { ) -> Result<()> {
let rt = Runtime::new()?; let rt = Runtime::new()?;

View file

@ -23,6 +23,7 @@ use crate::ui::theme;
mod controller; mod controller;
mod state; mod state;
mod util { mod util {
pub mod ansi;
pub mod config; pub mod config;
pub mod log; pub mod log;
} }

View file

@ -2,6 +2,7 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use druid::im::{HashMap, Vector}; use druid::im::{HashMap, Vector};
use druid::text::RichText;
use druid::{Data, ImageBuf, Lens, WindowHandle, WindowId}; use druid::{Data, ImageBuf, Lens, WindowHandle, WindowId};
use dtmt_shared::ModConfig; use dtmt_shared::ModConfig;
use nexusmods::Mod as NexusMod; use nexusmods::Mod as NexusMod;
@ -157,7 +158,7 @@ pub(crate) struct State {
pub nexus_api_key: Arc<String>, pub nexus_api_key: Arc<String>,
#[data(ignore)] #[data(ignore)]
pub log: Arc<String>, pub log: Vector<RichText>,
// True, when the initial loading of configuration and mods is still in progress // True, when the initial loading of configuration and mods is still in progress
pub loading: bool, pub loading: bool,
@ -194,7 +195,7 @@ impl State {
game_dir: Arc::new(PathBuf::new()), game_dir: Arc::new(PathBuf::new()),
data_dir: Arc::new(PathBuf::new()), data_dir: Arc::new(PathBuf::new()),
nexus_api_key: Arc::new(String::new()), nexus_api_key: Arc::new(String::new()),
log: Arc::new(String::new()), log: Vector::new(),
windows: HashMap::new(), windows: HashMap::new(),
loading: true, loading: true,
} }
@ -223,9 +224,4 @@ impl State {
pub fn can_move_mod_up(&self) -> bool { pub fn can_move_mod_up(&self) -> bool {
self.selected_mod_index.map(|i| i > 0).unwrap_or(false) 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);
}
} }

View file

@ -1,12 +1,14 @@
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use color_eyre::Report; use color_eyre::Report;
use druid::im::Vector;
use druid::{ use druid::{
im::Vector, AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target,
Target, WindowHandle, WindowId, WindowHandle, WindowId,
}; };
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use crate::util::ansi::ansi_to_rich_text;
use crate::{ui::window, util::config::Config}; use crate::{ui::window, util::config::Config};
use super::{ModInfo, State}; use super::{ModInfo, State};
@ -32,7 +34,7 @@ pub(crate) const ACTION_ADD_MOD: Selector<FileInfo> = Selector::new("dtmm.action
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<Arc<ModInfo>>> = pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<Arc<ModInfo>>> =
Selector::new("dtmm.action.finish-add-mod"); Selector::new("dtmm.action.finish-add-mod");
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = Selector::new("dtmm.action.log"); pub(crate) const ACTION_LOG: Selector<SingleUse<Vec<u8>>> = Selector::new("dtmm.action.log");
pub(crate) const ACTION_START_SAVE_SETTINGS: Selector = pub(crate) const ACTION_START_SAVE_SETTINGS: Selector =
Selector::new("dtmm.action.start-save-settings"); Selector::new("dtmm.action.start-save-settings");
@ -252,7 +254,8 @@ impl AppDelegate<State> for Delegate {
.get(ACTION_LOG) .get(ACTION_LOG)
.expect("command type matched but didn't contain the expected value"); .expect("command type matched but didn't contain the expected value");
if let Some(line) = line.take() { 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 Handled::Yes
} }

View file

@ -451,17 +451,18 @@ fn build_main() -> impl Widget<State> {
} }
fn build_log_view() -> impl Widget<State> { fn build_log_view() -> impl Widget<State> {
let font = FontDescriptor::new(FontFamily::MONOSPACE); let list = List::new(|| {
let label = Label::raw() Label::raw()
.with_font(font) .with_font(FontDescriptor::new(FontFamily::MONOSPACE))
.with_line_break_mode(LineBreaking::WordWrap) .with_line_break_mode(LineBreaking::WordWrap)
.lens(State::log) })
.padding(4.) .lens(State::log)
.scroll() .padding(4.)
.vertical() .scroll()
.controller(AutoScrollController); .vertical()
.controller(AutoScrollController);
let inner = Border::new(label) let inner = Border::new(list)
.with_color(theme::COLOR_FG2) .with_color(theme::COLOR_FG2)
.with_top_border(1.); .with_top_border(1.);

View file

@ -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<Color>,
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()
}

View file

@ -8,11 +8,11 @@ use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
pub struct ChannelWriter { pub struct ChannelWriter {
tx: UnboundedSender<String>, tx: UnboundedSender<Vec<u8>>,
} }
impl ChannelWriter { impl ChannelWriter {
pub fn new(tx: UnboundedSender<String>) -> Self { pub fn new(tx: UnboundedSender<Vec<u8>>) -> Self {
Self { tx } Self { tx }
} }
} }
@ -20,12 +20,9 @@ impl ChannelWriter {
impl std::io::Write for ChannelWriter { impl std::io::Write for ChannelWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let tx = self.tx.clone(); 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. // 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. // 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()) Ok(buf.len())
} }
@ -35,7 +32,7 @@ impl std::io::Write for ChannelWriter {
} }
} }
pub fn create_tracing_subscriber(tx: UnboundedSender<String>) { pub fn create_tracing_subscriber(tx: UnboundedSender<Vec<u8>>) {
let env_layer = if cfg!(debug_assertions) { let env_layer = if cfg!(debug_assertions) {
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")) EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))
} else { } else {