Merge pull request 'Colorize log output' (#97) from feat/color-log into master

Reviewed-on: #97
This commit is contained in:
Lucas Schwiderski 2023-04-05 16:27:25 +02:00
commit c1f4cd67ec
11 changed files with 246 additions and 44 deletions

110
Cargo.lock generated
View file

@ -38,6 +38,25 @@ 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 = "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"
@ -62,6 +81,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 +222,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 +352,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 +645,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 +838,7 @@ dependencies = [
name = "dtmm"
version = "0.1.0"
dependencies = [
"ansi-parser",
"bitflags",
"clap",
"color-eyre",
@ -874,6 +906,7 @@ dependencies = [
name = "dtmt-shared"
version = "0.1.0"
dependencies = [
"ansi_term",
"color-eyre",
"serde",
"steamlocate",
@ -1272,6 +1305,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 +1330,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 +1531,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 +1681,7 @@ dependencies = [
"serde",
"sized-chunks",
"typenum",
"version_check",
"version_check 0.9.4",
]
[[package]]
@ -2023,6 +2095,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 +2658,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check",
"version_check 0.9.4",
]
[[package]]
@ -2587,7 +2669,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
"version_check 0.9.4",
]
[[package]]
@ -3091,6 +3173,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 +3722,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 +3856,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"

View file

@ -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"

View file

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

View file

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

View file

@ -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;
@ -155,9 +156,7 @@ pub(crate) struct State {
pub game_dir: Arc<PathBuf>,
pub data_dir: Arc<PathBuf>,
pub nexus_api_key: Arc<String>,
#[data(ignore)]
pub log: Arc<String>,
pub log: Vector<RichText>,
// True, when the initial loading of configuration and mods is still in progress
pub loading: bool,
@ -194,7 +193,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 +222,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);
}
}

View file

@ -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<FileInfo> = Selector::new("dtmm.action
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<Arc<ModInfo>>> =
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 =
Selector::new("dtmm.action.start-save-settings");
@ -252,7 +254,8 @@ impl AppDelegate<State> 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.trim()));
}
Handled::Yes
}

View file

@ -451,17 +451,18 @@ fn build_main() -> impl Widget<State> {
}
fn build_log_view() -> impl Widget<State> {
let font = FontDescriptor::new(FontFamily::MONOSPACE);
let label = Label::raw()
.with_font(font)
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.);

View file

@ -0,0 +1,90 @@
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) => {
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;
pub struct ChannelWriter {
tx: UnboundedSender<String>,
tx: UnboundedSender<Vec<u8>>,
}
impl ChannelWriter {
pub fn new(tx: UnboundedSender<String>) -> Self {
pub fn new(tx: UnboundedSender<Vec<u8>>) -> Self {
Self { tx }
}
}
@ -20,12 +20,9 @@ impl ChannelWriter {
impl std::io::Write for ChannelWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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<String>) {
pub fn create_tracing_subscriber(tx: UnboundedSender<Vec<u8>>) {
let env_layer = if cfg!(debug_assertions) {
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))
} else {
@ -50,8 +47,6 @@ pub fn create_tracing_subscriber(tx: UnboundedSender<String>) {
};
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()))

View file

@ -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 = "*" }

View file

@ -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)?;