Skip to content

Commit

Permalink
ANSII Support & Bevy log capture (#74)
Browse files Browse the repository at this point in the history
* change readme

* fix CI
  • Loading branch information
makspll authored Sep 6, 2024
1 parent 2f36e81 commit 5d89a1e
Show file tree
Hide file tree
Showing 9 changed files with 434 additions and 36 deletions.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ readme = "README.md"

[dependencies]
bevy = { version = "0.14", default-features = false }
clap = { version = "4.5", features = ["derive"] }
clap = { version = "4.5", features = ["derive", "color", "help"] }
bevy_console_derive = { path = "./bevy_console_derive", version = "0.5.0" }
bevy_egui = "0.29.0"
shlex = "1.3"
ansi-parser = "0.9"
strip-ansi-escapes = "0.2"

[dev-dependencies]
bevy = { version = "0.14" }
color-print = { version = "0.3" }

[workspace]
members = ["bevy_console_derive"]
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ A simple *Half-Life* inspired console with support for argument parsing powered
<img src="https://raw.githubusercontent.com/richodemus/bevy-console/main/doc/screenshot.png" width="100%">
</p>

## Features
- [x] Command parsing with `clap`
- [x] Command history
- [x] Command completion
- [x] Support for ansii colors
- [x] Customizable key bindings
- [x] Customizable theme
- [x] Supports capturing Bevy logs to console

## Usage

Add `ConsolePlugin` and optionally the resource `ConsoleConfiguration`.
Expand Down Expand Up @@ -66,6 +75,7 @@ cargo run --example log_command
- [raw_commands](/examples/raw_commands.rs)
- [write_to_console](/examples/write_to_console.rs)
- [change_console_key](/examples/change_console_key.rs)
- [capture_bevy_logs](/examples/capture_bevy_logs.rs)

## wasm

Expand Down
23 changes: 23 additions & 0 deletions examples/capture_bevy_logs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use bevy::log::LogPlugin;
use bevy::{log, prelude::*};
use bevy_console::{make_layer, ConsolePlugin};

fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(LogPlugin {
level: log::Level::INFO,
filter: "error,capture_bevy_logs=info".to_owned(),
custom_layer: make_layer,
}),
ConsolePlugin,
))
.add_systems(Startup, || {
log::info!("Hi!");
log::warn!("This is a warning!");
log::debug!("You won't see me!");
log::error!("This is an error!");
log::info!("Bye!");
})
.run();
}
3 changes: 1 addition & 2 deletions run_all_examples.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#!/bin/bash

# read all env variables from wsl.env
export $(egrep -v '^#' wsl.env | xargs)

source wsl.env
find ./examples -type f -name "*.rs" -exec basename {} \; | while read file; do
echo "Running $file"
cargo run --example ${file%.rs}
Expand Down
248 changes: 248 additions & 0 deletions src/color.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
use std::collections::HashSet;

use ansi_parser::AnsiParser;
use bevy_egui::egui::Color32;

pub(crate) fn parse_ansi_styled_str(
ansi_string: &str,
) -> Vec<(usize, HashSet<TextFormattingOverride>)> {
let mut result: Vec<(usize, HashSet<TextFormattingOverride>)> = Vec::new();
let mut offset = 0;
for element in ansi_string.ansi_parse() {
match element {
ansi_parser::Output::TextBlock(t) => {
offset += t.len();
}
ansi_parser::Output::Escape(escape) => {
if let ansi_parser::AnsiSequence::SetGraphicsMode(mode) = escape {
let modes = parse_graphics_mode(mode.as_slice());
if let Some((last_offset, last)) = result.last_mut() {
if *last_offset == offset {
last.extend(modes);
continue;
}
}

result.push((offset, modes));
};
}
}
}
result
}

fn parse_graphics_mode(modes: &[u8]) -> HashSet<TextFormattingOverride> {
let mut results = HashSet::new();
for mode in modes.iter() {
let result = match *mode {
0 => TextFormattingOverride::Reset,
1 => TextFormattingOverride::Bold,
2 => TextFormattingOverride::Dim,
3 => TextFormattingOverride::Italic,
4 => TextFormattingOverride::Underline,
9 => TextFormattingOverride::Strikethrough,
30..=37 => TextFormattingOverride::Foreground(ansi_color_code_to_color32(mode - 30)),
40..=47 => TextFormattingOverride::Background(ansi_color_code_to_color32(mode - 40)),
_ => TextFormattingOverride::Reset,
};
results.insert(result);
}
results
}

fn ansi_color_code_to_color32(color_code: u8) -> Color32 {
match color_code {
1 => Color32::from_rgb(222, 56, 43), // red
2 => Color32::from_rgb(57, 181, 74), // green
3 => Color32::from_rgb(255, 199, 6), // yellow
4 => Color32::from_rgb(0, 111, 184), // blue
5 => Color32::from_rgb(118, 38, 113), // magenta
6 => Color32::from_rgb(44, 181, 233), // cyan
7 => Color32::from_rgb(204, 204, 204), // white
8 => Color32::from_rgb(128, 128, 128), // bright black
9 => Color32::from_rgb(255, 0, 0), // bright red
10 => Color32::from_rgb(0, 255, 0), // bright green
11 => Color32::from_rgb(255, 255, 0), // bright yellow
12 => Color32::from_rgb(0, 0, 255), // bright blue
13 => Color32::from_rgb(255, 0, 255), // bright magenta
14 => Color32::from_rgb(0, 255, 255), // bright cyan
15 => Color32::from_rgb(255, 255, 255), // bright white
_ => Color32::from_rgb(1, 1, 1), // black
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum TextFormattingOverride {
Reset,
Bold,
Dim,
Italic,
Underline,
Strikethrough,
Foreground(Color32),
Background(Color32),
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_bold_text() {
let ansi_string = color_print::cstr!(r#"<bold>12345</bold>"#);
let result = parse_ansi_styled_str(ansi_string);
assert_eq!(
result,
vec![
(0, HashSet::from([TextFormattingOverride::Bold])),
(5, HashSet::from([TextFormattingOverride::Reset]))
]
);
}

#[test]
fn test_underlined_text() {
let ansi_string = color_print::cstr!(r#"<underline>12345</underline>"#);
let result = parse_ansi_styled_str(ansi_string);
assert_eq!(
result,
vec![
(0, HashSet::from([TextFormattingOverride::Underline])),
(5, HashSet::from([TextFormattingOverride::Reset]))
]
);
}

#[test]
fn test_italics_text() {
let ansi_string = color_print::cstr!(r#"<italic>12345</italic>"#);
let result = parse_ansi_styled_str(ansi_string);
assert_eq!(
result,
vec![
(0, HashSet::from([TextFormattingOverride::Italic])),
(5, HashSet::from([TextFormattingOverride::Reset]))
]
);
}

#[test]
fn test_dim_text() {
let ansi_string = color_print::cstr!(r#"<dim>12345</dim>"#);
let result = parse_ansi_styled_str(ansi_string);
assert_eq!(
result,
vec![
(0, HashSet::from([TextFormattingOverride::Dim])),
(5, HashSet::from([TextFormattingOverride::Reset]))
]
);
}

#[test]
fn test_strikethrough_text() {
let ansi_string = color_print::cstr!(r#"<strike>12345</strike>"#);
let result = parse_ansi_styled_str(ansi_string);
assert_eq!(
result,
vec![
(0, HashSet::from([TextFormattingOverride::Strikethrough])),
(5, HashSet::from([TextFormattingOverride::Reset]))
]
);
}

#[test]
fn test_foreground_color() {
let ansi_string = color_print::cstr!(r#"<red>12345</red>"#);
let result = parse_ansi_styled_str(ansi_string);
assert_eq!(
result,
vec![
(
0,
HashSet::from([TextFormattingOverride::Foreground(Color32::from_rgb(
222, 56, 43
))])
),
(5, HashSet::from([TextFormattingOverride::Reset]))
]
);
}

#[test]
fn test_background_color() {
let ansi_string = color_print::cstr!(r#"<bg:red>12345</bg:red>"#);
let result = parse_ansi_styled_str(ansi_string);
assert_eq!(
result,
vec![
(
0,
HashSet::from([TextFormattingOverride::Background(Color32::from_rgb(
222, 56, 43
))])
),
(5, HashSet::from([TextFormattingOverride::Reset]))
]
);
}

#[test]
fn test_multiple_styles() {
let ansi_string = color_print::cstr!(r#"<bold><red>12345</red></bold>"#);
let result = parse_ansi_styled_str(ansi_string);
assert_eq!(
result,
vec![
(
0,
HashSet::from([
TextFormattingOverride::Foreground(Color32::from_rgb(222, 56, 43)),
TextFormattingOverride::Bold,
])
),
(5, HashSet::from([TextFormattingOverride::Reset]))
]
);
}

#[test]
fn non_overlapping_styles() {
let ansi_string = color_print::cstr!(r#"<bold>12345</bold><red>12345</red>"#);
let result = parse_ansi_styled_str(ansi_string);
assert_eq!(
result,
vec![
(0, HashSet::from([TextFormattingOverride::Bold])),
(
5,
HashSet::from([
TextFormattingOverride::Reset,
TextFormattingOverride::Foreground(Color32::from_rgb(222, 56, 43))
])
),
(10, HashSet::from([TextFormattingOverride::Reset]))
]
);
}

#[test]
fn overlapping_non_symmetric_styles() {
let ansi_string = color_print::cstr!(r#"<bold>12345<red>12345</red></bold>"#);
let result = parse_ansi_styled_str(ansi_string);
assert_eq!(
result,
vec![
(0, HashSet::from([TextFormattingOverride::Bold])),
(
5,
HashSet::from([TextFormattingOverride::Foreground(Color32::from_rgb(
222, 56, 43
))])
),
(10, HashSet::from([TextFormattingOverride::Reset]))
]
);
}
}
Loading

0 comments on commit 5d89a1e

Please sign in to comment.