mirror of
https://github.com/sharkdp/hexyl.git
synced 2026-10-26 20:16:34 +00:00
Enable custom colors with environment variables (#248)
* Enable custom colors with environment variables * Rename HEXYL_XYZ env vars to HEXYL_COLOR_XYZ
This commit is contained in:
1 parent
6220cae644
commit
9ef7c346dd
7 files changed
+268
-32
No files matched your search
+1
-1
@@ -9,7 +9,7 @@ readme = "README.md"
|
||||
repository = "https://github.com/sharkdp/hexyl"
|
||||
version = "0.16.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.74"
|
||||
rust-version = "1.80"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
|
||||
@@ -193,6 +193,22 @@ scoop install hexyl
|
||||
x env use hexyl
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`hexyl` colors can be configured via environment variables. The variables used are as follows:
|
||||
|
||||
* `HEXYL_COLOR_ASCII_PRINTABLE`: Any non-whitespace printable ASCII character
|
||||
* `HEXYL_COLOR_ASCII_WHITESPACE`: Whitespace such as space or newline (only visible in middle panel with byte values)
|
||||
* `HEXYL_COLOR_ASCII_OTHER`: Any other ASCII character (< `0x80`) besides null
|
||||
* `HEXYL_COLOR_NULL`: The null byte (`0x00`)
|
||||
* `HEXYL_COLOR_NONASCII`: Any non-ASCII byte (> `0x7F`)
|
||||
* `HEXYL_COLOR_OFFSET`: The lefthand file offset
|
||||
|
||||
The colors can be any of the 8 standard terminal colors: `black`, `blue`, `cyan`, `green`, `magenta`, `red`,
|
||||
`yellow` and `white`. The "bright" variants are also supported (e.g., `bright blue`). Additionally, you can use
|
||||
the RGB hex format, `#abcdef`. For example, `HEXYL_COLOR_ASCII_PRINTABLE=blue HEXYL_COLOR_ASCII_WHITESPACE="bright green"
|
||||
HEXYL_COLOR_ASCII_OTHER="#ff7f99"`.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
@@ -106,6 +106,22 @@ _FILE_
|
||||
**-V**, **\--version**
|
||||
: Prints version information.
|
||||
|
||||
# ENVIRONMENT VARIABLES
|
||||
|
||||
**hexyl** colors can be configured via environment variables. The variables used are as follows:
|
||||
|
||||
: - **HEXYL_COLOR_ASCII_PRINTABLE**: Any non-whitespace printable ASCII character
|
||||
- **HEXYL_COLOR_ASCII_WHITESPACE**: Whitespace such as space or newline (only visible in middle panel with byte values)
|
||||
- **HEXYL_COLOR_ASCII_OTHER**: Any other ASCII character (< **0x80**) besides null
|
||||
- **HEXYL_COLOR_NULL**: The null byte (**0x00**)
|
||||
- **HEXYL_COLOR_NONASCII**: Any non-ASCII byte (> **0x7F**)
|
||||
- **HEXYL_COLOR_OFFSET**: The lefthand file offset
|
||||
|
||||
The colors can be any of the 8 standard terminal colors: **black**, **blue**, **cyan**, **green**, **magenta**, **red**,
|
||||
**yellow** and **white**. The "bright" variants are also supported (e.g., **bright blue**). Additionally, you can use
|
||||
the RGB hex format, **#abcdef**. For example, **HEXYL_COLOR_ASCII_PRINTABLE=blue HEXYL_COLOR_ASCII_WHITESPACE="bright green"
|
||||
HEXYL_COLOR_ASCII_OTHER="#ff7f99"**.
|
||||
|
||||
# NOTES
|
||||
|
||||
Source repository:
|
||||
|
||||
+35
-8
@@ -1,12 +1,39 @@
|
||||
use owo_colors::{colors, Color};
|
||||
use owo_colors::{colors, AnsiColors, Color, DynColors, OwoColorize};
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub const COLOR_NULL: &[u8] = colors::BrightBlack::ANSI_FG.as_bytes();
|
||||
pub const COLOR_OFFSET: &[u8] = colors::BrightBlack::ANSI_FG.as_bytes();
|
||||
pub const COLOR_ASCII_PRINTABLE: &[u8] = colors::Cyan::ANSI_FG.as_bytes();
|
||||
pub const COLOR_ASCII_WHITESPACE: &[u8] = colors::Green::ANSI_FG.as_bytes();
|
||||
pub const COLOR_ASCII_OTHER: &[u8] = colors::Green::ANSI_FG.as_bytes();
|
||||
pub const COLOR_NONASCII: &[u8] = colors::Yellow::ANSI_FG.as_bytes();
|
||||
pub const COLOR_RESET: &[u8] = colors::Default::ANSI_FG.as_bytes();
|
||||
pub static COLOR_NULL: LazyLock<String> =
|
||||
LazyLock::new(|| init_color("NULL", AnsiColors::BrightBlack));
|
||||
pub static COLOR_OFFSET: LazyLock<String> =
|
||||
LazyLock::new(|| init_color("OFFSET", AnsiColors::BrightBlack));
|
||||
pub static COLOR_ASCII_PRINTABLE: LazyLock<String> =
|
||||
LazyLock::new(|| init_color("ASCII_PRINTABLE", AnsiColors::Cyan));
|
||||
pub static COLOR_ASCII_WHITESPACE: LazyLock<String> =
|
||||
LazyLock::new(|| init_color("ASCII_WHITESPACE", AnsiColors::Green));
|
||||
pub static COLOR_ASCII_OTHER: LazyLock<String> =
|
||||
LazyLock::new(|| init_color("ASCII_OTHER", AnsiColors::Green));
|
||||
pub static COLOR_NONASCII: LazyLock<String> =
|
||||
LazyLock::new(|| init_color("NONASCII", AnsiColors::Yellow));
|
||||
pub const COLOR_RESET: &str = colors::Default::ANSI_FG;
|
||||
|
||||
fn init_color(name: &str, default_ansi: AnsiColors) -> String {
|
||||
let default = DynColors::Ansi(default_ansi);
|
||||
let env_var = format!("HEXYL_COLOR_{}", name);
|
||||
let color = match std::env::var(env_var).as_deref() {
|
||||
Ok(color) => match DynColors::from_str(color) {
|
||||
Ok(color) => color,
|
||||
_ => default,
|
||||
},
|
||||
_ => default,
|
||||
};
|
||||
// owo_colors' API isn't designed to get the terminal codes directly for
|
||||
// dynamic colors, so we use this hack to get them from the LHS of some text.
|
||||
format!("{}", "|".color(color))
|
||||
.split_once("|")
|
||||
.unwrap()
|
||||
.0
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub const CP437: [char; 256] = [
|
||||
|
||||
+13
-13
@@ -84,11 +84,11 @@ impl Byte {
|
||||
fn color(self) -> &'static [u8] {
|
||||
use crate::ByteCategory::*;
|
||||
match self.category() {
|
||||
Null => COLOR_NULL,
|
||||
AsciiPrintable => COLOR_ASCII_PRINTABLE,
|
||||
AsciiWhitespace => COLOR_ASCII_WHITESPACE,
|
||||
AsciiOther => COLOR_ASCII_OTHER,
|
||||
NonAscii => COLOR_NONASCII,
|
||||
Null => COLOR_NULL.as_bytes(),
|
||||
AsciiPrintable => COLOR_ASCII_PRINTABLE.as_bytes(),
|
||||
AsciiWhitespace => COLOR_ASCII_WHITESPACE.as_bytes(),
|
||||
AsciiOther => COLOR_ASCII_OTHER.as_bytes(),
|
||||
NonAscii => COLOR_NONASCII.as_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,14 +440,14 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
|
||||
.as_bytes(),
|
||||
)?;
|
||||
if self.show_color {
|
||||
self.writer.write_all(COLOR_OFFSET)?;
|
||||
self.writer.write_all(COLOR_OFFSET.as_bytes())?;
|
||||
}
|
||||
if self.show_position_panel {
|
||||
match self.squeezer {
|
||||
Squeezer::Print => {
|
||||
self.writer.write_all(b"*")?;
|
||||
if self.show_color {
|
||||
self.writer.write_all(COLOR_RESET)?;
|
||||
self.writer.write_all(COLOR_RESET.as_bytes())?;
|
||||
}
|
||||
self.writer.write_all(b" ")?;
|
||||
}
|
||||
@@ -462,7 +462,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
|
||||
.write_all(self.byte_hex_panel_g[byte as usize].as_bytes())?;
|
||||
}
|
||||
if self.show_color {
|
||||
self.writer.write_all(COLOR_RESET)?;
|
||||
self.writer.write_all(COLOR_RESET.as_bytes())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,7 +494,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
|
||||
}
|
||||
if i == 8 * self.panels - 1 {
|
||||
if self.show_color {
|
||||
self.writer.write_all(COLOR_RESET)?;
|
||||
self.writer.write_all(COLOR_RESET.as_bytes())?;
|
||||
self.curr_color = None;
|
||||
}
|
||||
self.writer.write_all(
|
||||
@@ -505,7 +505,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
|
||||
)?;
|
||||
} else if i % 8 == 7 {
|
||||
if self.show_color {
|
||||
self.writer.write_all(COLOR_RESET)?;
|
||||
self.writer.write_all(COLOR_RESET.as_bytes())?;
|
||||
self.curr_color = None;
|
||||
}
|
||||
self.writer.write_all(
|
||||
@@ -531,12 +531,12 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
|
||||
Squeezer::Print => {
|
||||
if !self.show_position_panel && i == 0 {
|
||||
if self.show_color {
|
||||
self.writer.write_all(COLOR_OFFSET)?;
|
||||
self.writer.write_all(COLOR_OFFSET.as_bytes())?;
|
||||
}
|
||||
self.writer
|
||||
.write_all(self.byte_char_panel[b'*' as usize].as_bytes())?;
|
||||
if self.show_color {
|
||||
self.writer.write_all(COLOR_RESET)?;
|
||||
self.writer.write_all(COLOR_RESET.as_bytes())?;
|
||||
}
|
||||
} else if i % (self.group_size as usize) == 0 {
|
||||
self.writer.write_all(b" ")?;
|
||||
@@ -562,7 +562,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
|
||||
if i % 8 == 7 {
|
||||
if self.show_color {
|
||||
self.curr_color = None;
|
||||
self.writer.write_all(COLOR_RESET)?;
|
||||
self.writer.write_all(COLOR_RESET.as_bytes())?;
|
||||
}
|
||||
self.writer.write_all(b" ")?;
|
||||
// byte is last in last panel
|
||||
|
||||
+10
-10
@@ -493,32 +493,32 @@ fn print_color_table() -> io::Result<()> {
|
||||
writeln!(stdout, "hexyl color reference:\n")?;
|
||||
|
||||
// NULL bytes
|
||||
stdout.write_all(COLOR_NULL)?;
|
||||
stdout.write_all(COLOR_NULL.as_bytes())?;
|
||||
writeln!(stdout, "⋄ NULL bytes (0x00)")?;
|
||||
stdout.write_all(COLOR_RESET)?;
|
||||
stdout.write_all(COLOR_RESET.as_bytes())?;
|
||||
|
||||
// ASCII printable
|
||||
stdout.write_all(COLOR_ASCII_PRINTABLE)?;
|
||||
stdout.write_all(COLOR_ASCII_PRINTABLE.as_bytes())?;
|
||||
writeln!(stdout, "a ASCII printable characters (0x20 - 0x7E)")?;
|
||||
stdout.write_all(COLOR_RESET)?;
|
||||
stdout.write_all(COLOR_RESET.as_bytes())?;
|
||||
|
||||
// ASCII whitespace
|
||||
stdout.write_all(COLOR_ASCII_WHITESPACE)?;
|
||||
stdout.write_all(COLOR_ASCII_WHITESPACE.as_bytes())?;
|
||||
writeln!(stdout, "_ ASCII whitespace (0x09 - 0x0D, 0x20)")?;
|
||||
stdout.write_all(COLOR_RESET)?;
|
||||
stdout.write_all(COLOR_RESET.as_bytes())?;
|
||||
|
||||
// ASCII other
|
||||
stdout.write_all(COLOR_ASCII_OTHER)?;
|
||||
stdout.write_all(COLOR_ASCII_OTHER.as_bytes())?;
|
||||
writeln!(
|
||||
stdout,
|
||||
"• ASCII control characters (except NULL and whitespace)"
|
||||
)?;
|
||||
stdout.write_all(COLOR_RESET)?;
|
||||
stdout.write_all(COLOR_RESET.as_bytes())?;
|
||||
|
||||
// Non-ASCII
|
||||
stdout.write_all(COLOR_NONASCII)?;
|
||||
stdout.write_all(COLOR_NONASCII.as_bytes())?;
|
||||
writeln!(stdout, "× Non-ASCII bytes (0x80 - 0xFF)")?;
|
||||
stdout.write_all(COLOR_RESET)?;
|
||||
stdout.write_all(COLOR_RESET.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -785,3 +785,180 @@ mod character_table {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod colors {
|
||||
use super::hexyl;
|
||||
use owo_colors::{colors, Color};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// This is a helper for testing color in output. Writing tests to expect
|
||||
// raw color codes is ugly and hard to look at. Loading expected output
|
||||
// from files works fine, but you end up with a lot of files for all the
|
||||
// tests, and you have to cross-reference the file with the test that uses
|
||||
// it. The files also suffer from the same problem of being hard to
|
||||
// visually inspect. Just catting the file to see the colorized output
|
||||
// loses the nuance of where exactly the color codes appear (before or
|
||||
// after spaces, for example), or whether there are redundant codes.
|
||||
//
|
||||
// So this ColorMap solves the problem neatly by having two inputs:
|
||||
// - the easy to read expected output in plain format without any colors
|
||||
// - a mapping with identical structure except some characters replaced
|
||||
// with single character color codes.
|
||||
// This makes it easy to reference the output and expected colors side by
|
||||
// side, and provides fairly precise control over exactly where color codes
|
||||
// are expected (the only caveat being you can't have two color codes back
|
||||
// to back). ColorMap combines these into the actual expected output.
|
||||
//
|
||||
// The color mapping needs to be identical to the expected output, except
|
||||
// it has some chars replaced by color code stand ins. These are replaced
|
||||
// with the actual color codes by the colorize method. The '.' character
|
||||
// is also ignored (it doesn't need to match the input). This makes the
|
||||
// color map more readable and avoids input characters from conflicting
|
||||
// with color chars.
|
||||
struct ColorMap {
|
||||
text_map: &'static str,
|
||||
char_to_color: HashMap<char, &'static str>,
|
||||
}
|
||||
|
||||
impl ColorMap {
|
||||
fn from(text_map: &'static str) -> Self {
|
||||
ColorMap {
|
||||
text_map,
|
||||
char_to_color: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with<C: Color>(&mut self, c: char) -> &mut Self {
|
||||
self.char_to_color.insert(c, C::ANSI_FG);
|
||||
self
|
||||
}
|
||||
|
||||
fn colorize(&self, input: &str) -> String {
|
||||
let mut output = String::new();
|
||||
let mut input_chars = input.chars();
|
||||
for c in self.text_map.chars() {
|
||||
let next_input = input_chars.next().expect("input and color map don't match");
|
||||
if let Some(color) = self.char_to_color.get(&c) {
|
||||
output.push_str(color);
|
||||
} else if c != '.' {
|
||||
// ignore '.' in the mapping for readability
|
||||
assert_eq!(c, next_input, "input and color map don't match");
|
||||
}
|
||||
output.push(next_input);
|
||||
}
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_colors() {
|
||||
let input = b"He\x11\0 \xff\0\xdd";
|
||||
let expected_text = "\
|
||||
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
|
||||
│00000000│ 48 65 11 00 20 ff 00 dd ┊ │He•⋄ ×⋄×┊ │\n\
|
||||
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n";
|
||||
let expected = ColorMap::from(
|
||||
"\
|
||||
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
|
||||
│r.......d y. .. b. c. g. m. c. m.d┊ d│y.bcgmcmd d\n\
|
||||
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
|
||||
)
|
||||
.with::<colors::Red>('r')
|
||||
.with::<colors::Default>('d')
|
||||
.with::<colors::Yellow>('y')
|
||||
.with::<colors::Blue>('b')
|
||||
.with::<colors::Green>('g')
|
||||
.with::<colors::BrightMagenta>('m')
|
||||
.with::<colors::CustomColor<0xab, 0xcd, 0xef>>('c')
|
||||
.colorize(expected_text);
|
||||
|
||||
hexyl()
|
||||
.write_stdin(input)
|
||||
.arg("--color=always")
|
||||
.env("HEXYL_COLOR_OFFSET", "red")
|
||||
.env("HEXYL_COLOR_ASCII_PRINTABLE", "yellow")
|
||||
.env("HEXYL_COLOR_ASCII_WHITESPACE", "green")
|
||||
.env("HEXYL_COLOR_ASCII_OTHER", "blue")
|
||||
.env("HEXYL_COLOR_NONASCII", "bright magenta")
|
||||
.env("HEXYL_COLOR_NULL", "#abcdef")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_colors() {
|
||||
let input = b"He\x11\0 \xff\0\xdd";
|
||||
let expected_text = "\
|
||||
┌────────┬─────────────────────────────────────────────────────────────────────────┬────────┐\n\
|
||||
│00000000│ 01001000 01100101 00010001 00000000 00100000 11111111 00000000 11011101 │He•⋄ ×⋄×│\n\
|
||||
└────────┴─────────────────────────────────────────────────────────────────────────┴────────┘\n";
|
||||
let expected = ColorMap::from(
|
||||
"\
|
||||
┌────────┬─────────────────────────────────────────────────────────────────────────┬────────┐\n\
|
||||
│r.......d y....... ........ b....... c....... g....... m....... c....... m.......d│y.bcgmcmd\n\
|
||||
└────────┴─────────────────────────────────────────────────────────────────────────┴────────┘\n"
|
||||
)
|
||||
.with::<colors::Red>('r')
|
||||
.with::<colors::Default>('d')
|
||||
.with::<colors::Yellow>('y')
|
||||
.with::<colors::Blue>('b')
|
||||
.with::<colors::Green>('g')
|
||||
.with::<colors::BrightMagenta>('m')
|
||||
.with::<colors::CustomColor<0xab, 0xcd, 0xef>>('c')
|
||||
.colorize(expected_text);
|
||||
|
||||
hexyl()
|
||||
.write_stdin(input)
|
||||
.arg("--color=always")
|
||||
.arg("--panels=1")
|
||||
.arg("--base=binary")
|
||||
.env("HEXYL_COLOR_OFFSET", "red")
|
||||
.env("HEXYL_COLOR_ASCII_PRINTABLE", "yellow")
|
||||
.env("HEXYL_COLOR_ASCII_WHITESPACE", "green")
|
||||
.env("HEXYL_COLOR_ASCII_OTHER", "blue")
|
||||
.env("HEXYL_COLOR_NONASCII", "bright magenta")
|
||||
.env("HEXYL_COLOR_NULL", "#abcdef")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groupsize_colors() {
|
||||
let input = b"He\x11\0 \xff\0\xdd";
|
||||
let expected_text = "\
|
||||
┌────────┬─────────────────────┬────────┐\n\
|
||||
│00000000│ 4865 1100 20ff 00dd │He•⋄ ×⋄×│\n\
|
||||
└────────┴─────────────────────┴────────┘\n";
|
||||
let expected = ColorMap::from(
|
||||
"\
|
||||
┌────────┬─────────────────────┬────────┐\n\
|
||||
│r.......d y... b.c. g.m. c.m.d│y.bcgmcmd\n\
|
||||
└────────┴─────────────────────┴────────┘\n",
|
||||
)
|
||||
.with::<colors::Red>('r')
|
||||
.with::<colors::Default>('d')
|
||||
.with::<colors::Yellow>('y')
|
||||
.with::<colors::Blue>('b')
|
||||
.with::<colors::Green>('g')
|
||||
.with::<colors::BrightMagenta>('m')
|
||||
.with::<colors::CustomColor<0xab, 0xcd, 0xef>>('c')
|
||||
.colorize(expected_text);
|
||||
|
||||
hexyl()
|
||||
.write_stdin(input)
|
||||
.arg("--color=always")
|
||||
.arg("--panels=1")
|
||||
.arg("--groupsize=2")
|
||||
.env("HEXYL_COLOR_OFFSET", "red")
|
||||
.env("HEXYL_COLOR_ASCII_PRINTABLE", "yellow")
|
||||
.env("HEXYL_COLOR_ASCII_WHITESPACE", "green")
|
||||
.env("HEXYL_COLOR_ASCII_OTHER", "blue")
|
||||
.env("HEXYL_COLOR_NONASCII", "bright magenta")
|
||||
.env("HEXYL_COLOR_NULL", "#abcdef")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(expected);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user