Add new braille character table and color schemes (#247)

* Add new braille character table and color schemes

* Add character for tab and change color for 0x7f

---------

Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
Niclas SchwarzloseandDavid Peter authored and GitHub committed 2025-08-31 21:13:57 +02:00
1 parent 9ef7c346dd
commit f2547aed69
4 files changed
+179 -14

No files matched your search

+3
View File
@@ -4,6 +4,9 @@
## Features
- Add `--color-scheme` option, see #247 (@aticu)
- Add `braille` character table, see #247 (@aticu)
## Bugfixes
+75
View File
@@ -35,6 +35,81 @@ fn init_color(name: &str, default_ansi: AnsiColors) -> String {
.to_owned()
}
pub const COLOR_NULL_RGB: &[u8] = &rgb_bytes(100, 100, 100);
pub const COLOR_DEL: &[u8] = &rgb_bytes(64, 128, 0);
pub const COLOR_GRADIENT_NONASCII: [[u8; 19]; 128] =
generate_color_gradient(&[(255, 0, 0, 0.0), (255, 255, 0, 0.66), (255, 255, 255, 1.0)]);
pub const COLOR_GRADIENT_ASCII_NONPRINTABLE: [[u8; 19]; 31] =
generate_color_gradient(&[(255, 0, 255, 0.0), (128, 0, 255, 1.0)]);
pub const COLOR_GRADIENT_ASCII_PRINTABLE: [[u8; 19]; 95] =
generate_color_gradient(&[(0, 128, 255, 0.0), (0, 255, 128, 1.0)]);
const fn as_dec(byte: u8) -> [u8; 3] {
[
b'0' + (byte / 100),
b'0' + ((byte % 100) / 10),
b'0' + (byte % 10),
]
}
const fn rgb_bytes(r: u8, g: u8, b: u8) -> [u8; 19] {
let mut buf = *b"\x1b[38;2;rrr;ggg;bbbm";
// r 7
buf[7] = as_dec(r)[0];
buf[8] = as_dec(r)[1];
buf[9] = as_dec(r)[2];
// g 11
buf[11] = as_dec(g)[0];
buf[12] = as_dec(g)[1];
buf[13] = as_dec(g)[2];
// b 15
buf[15] = as_dec(b)[0];
buf[16] = as_dec(b)[1];
buf[17] = as_dec(b)[2];
buf
}
const fn generate_color_gradient<const N: usize>(stops: &[(u8, u8, u8, f64)]) -> [[u8; 19]; N] {
let mut out = [rgb_bytes(0, 0, 0); N];
assert!(stops.len() >= 2, "need at least two stops for the gradient");
let mut byte = 0;
while byte < N {
let relative_byte = byte as f64 / N as f64;
let mut i = 1;
while i < stops.len() && stops[i].3 < relative_byte {
i += 1;
}
if i >= stops.len() {
i = stops.len() - 1;
}
let prev_stop = stops[i - 1];
let stop = stops[i];
let diff = stop.3 - prev_stop.3;
let t = (relative_byte - prev_stop.3) / diff;
let r = (prev_stop.0 as f64 + (t * (stop.0 as f64 - prev_stop.0 as f64))) as u8;
let g = (prev_stop.1 as f64 + (t * (stop.1 as f64 - prev_stop.1 as f64))) as u8;
let b = (prev_stop.2 as f64 + (t * (stop.2 as f64 - prev_stop.2 as f64))) as u8;
out[byte] = rgb_bytes(r, g, b);
byte += 1;
}
out
}
#[rustfmt::skip]
pub const CP437: [char; 256] = [
// Copyright (c) 2016, Delan Azabani <delan@azabani.com>
+93 -13
View File
@@ -43,6 +43,24 @@ pub enum CharacterTable {
/// Uses code page 437 (for non-ASCII bytes).
#[value(name = "codepage-437")]
CP437,
/// Uses braille characters for non-printable bytes.
Braille,
}
#[derive(Copy, Clone, Debug, Default, ValueEnum)]
#[non_exhaustive]
pub enum ColorScheme {
/// Show the default colors: bright black for NULL bytes, green for ASCII
/// space characters and non-printable ASCII, cyan for printable ASCII characters,
/// and yellow for non-ASCII bytes.
#[default]
Default,
/// Show bright black for NULL bytes, cyan for printable ASCII characters, a gradient
/// from pink to violet for non-printable ASCII characters and a heatmap-like gradient
/// from red to yellow to white for non-ASCII bytes.
Gradient,
}
#[derive(Copy, Clone, Debug, Default, ValueEnum)]
@@ -81,14 +99,29 @@ impl Byte {
}
}
fn color(self) -> &'static [u8] {
fn color(self, color_scheme: ColorScheme) -> &'static [u8] {
use crate::ByteCategory::*;
match self.category() {
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(),
match color_scheme {
ColorScheme::Default => match self.category() {
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(),
},
ColorScheme::Gradient => match self.category() {
Null => COLOR_NULL_RGB,
AsciiWhitespace if self.0 == b' ' => &COLOR_GRADIENT_ASCII_PRINTABLE[0],
AsciiPrintable => &COLOR_GRADIENT_ASCII_PRINTABLE[(self.0 - b' ') as usize],
AsciiWhitespace | AsciiOther => {
if self.0 == 0x7f {
COLOR_DEL
} else {
&COLOR_GRADIENT_ASCII_NONPRINTABLE[self.0 as usize - 1]
}
}
NonAscii => &COLOR_GRADIENT_NONASCII[(self.0 - 128) as usize],
},
}
}
@@ -113,6 +146,37 @@ impl Byte {
},
CharacterTable::CP1047 => CP1047[self.0 as usize],
CharacterTable::CP437 => CP437[self.0 as usize],
CharacterTable::Braille => match self.category() {
// null is important enough to get its own symbol
Null => '⋄',
AsciiPrintable => self.0 as char,
AsciiWhitespace if self.0 == b' ' => ' ',
// `\t`, `\n` and `\r` are important enough to get their own symbols
AsciiWhitespace if self.0 == b'\t' => '→',
AsciiWhitespace if self.0 == b'\n' => '↵',
AsciiWhitespace if self.0 == b'\r' => '←',
AsciiWhitespace | AsciiOther | NonAscii => {
/// Adjust the bits from the original number to a new number.
///
/// Bit positions in braille are adjusted as follows:
///
/// ```text
/// 0 3 => 0 1
/// 1 4 => 2 3
/// 2 5 => 4 5
/// 6 7 => 6 7
/// ```
fn to_braille_bits(byte: u8) -> u8 {
let mut out = 0;
for (from, to) in [0, 3, 1, 4, 2, 5, 6, 7].into_iter().enumerate() {
out |= (byte >> from & 1) << to;
}
out
}
char::from_u32(0x2800 + to_braille_bits(self.0) as u32).unwrap()
}
},
}
}
}
@@ -203,6 +267,7 @@ pub struct PrinterBuilder<'a, Writer: Write> {
base: Base,
endianness: Endianness,
character_table: CharacterTable,
color_scheme: ColorScheme,
}
impl<'a, Writer: Write> PrinterBuilder<'a, Writer> {
@@ -219,6 +284,7 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> {
base: Base::Hexadecimal,
endianness: Endianness::Big,
character_table: CharacterTable::Default,
color_scheme: ColorScheme::Default,
}
}
@@ -272,6 +338,11 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> {
self
}
pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
self.color_scheme = color_scheme;
self
}
pub fn build(self) -> Printer<'a, Writer> {
Printer::new(
self.writer,
@@ -285,6 +356,7 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> {
self.base,
self.endianness,
self.character_table,
self.color_scheme,
)
}
}
@@ -298,6 +370,7 @@ pub struct Printer<'a, Writer: Write> {
show_position_panel: bool,
show_color: bool,
curr_color: Option<&'static [u8]>,
color_scheme: ColorScheme,
border_style: BorderStyle,
byte_hex_panel: Vec<String>,
byte_char_panel: Vec<String>,
@@ -329,6 +402,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
base: Base,
endianness: Endianness,
character_table: CharacterTable,
color_scheme: ColorScheme,
) -> Printer<'a, Writer> {
Printer {
idx: 0,
@@ -338,6 +412,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
show_position_panel,
show_color,
curr_color: None,
color_scheme,
border_style,
byte_hex_panel: (0u8..=u8::MAX)
.map(|i| match base {
@@ -481,9 +556,10 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
Squeezer::Print | Squeezer::Delete => self.writer.write_all(b" ")?,
Squeezer::Ignore | Squeezer::Disabled => {
if let Some(&b) = self.line_buf.get(i as usize) {
if self.show_color && self.curr_color != Some(Byte(b).color()) {
self.writer.write_all(Byte(b).color())?;
self.curr_color = Some(Byte(b).color());
if self.show_color && self.curr_color != Some(Byte(b).color(self.color_scheme))
{
self.writer.write_all(Byte(b).color(self.color_scheme))?;
self.curr_color = Some(Byte(b).color(self.color_scheme));
}
self.writer
.write_all(self.byte_char_panel[b as usize].as_bytes())?;
@@ -550,9 +626,9 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
if i % (self.group_size as usize) == 0 {
self.writer.write_all(b" ")?;
}
if self.show_color && self.curr_color != Some(Byte(b).color()) {
self.writer.write_all(Byte(b).color())?;
self.curr_color = Some(Byte(b).color());
if self.show_color && self.curr_color != Some(Byte(b).color(self.color_scheme)) {
self.writer.write_all(Byte(b).color(self.color_scheme))?;
self.curr_color = Some(Byte(b).color(self.color_scheme));
}
self.writer
.write_all(self.byte_hex_panel[b as usize].as_bytes())?;
@@ -780,6 +856,7 @@ mod tests {
Base::Hexadecimal,
Endianness::Big,
CharacterTable::Default,
ColorScheme::Default,
);
printer.print_all(input).unwrap();
@@ -836,6 +913,7 @@ mod tests {
Base::Hexadecimal,
Endianness::Big,
CharacterTable::Default,
ColorScheme::Default,
);
printer.display_offset(0xdeadbeef);
@@ -871,6 +949,7 @@ mod tests {
Base::Hexadecimal,
Endianness::Big,
CharacterTable::Default,
ColorScheme::Default,
);
printer.print_all(input).unwrap();
@@ -932,6 +1011,7 @@ mod tests {
Base::Hexadecimal,
Endianness::Big,
CharacterTable::Default,
ColorScheme::Default,
);
printer.print_all(input).unwrap();
+8 -1
View File
@@ -14,7 +14,7 @@ use thiserror::Error as ThisError;
use terminal_size::terminal_size;
use hexyl::{Base, BorderStyle, CharacterTable, Endianness, Input, PrinterBuilder};
use hexyl::{Base, BorderStyle, CharacterTable, ColorScheme, Endianness, Input, PrinterBuilder};
use hexyl::{
COLOR_ASCII_OTHER, COLOR_ASCII_PRINTABLE, COLOR_ASCII_WHITESPACE, COLOR_NONASCII, COLOR_NULL,
@@ -129,6 +129,10 @@ struct Opt {
#[arg(long, value_enum, default_value_t, value_name("FORMAT"))]
character_table: CharacterTable,
/// Defines the color scheme for the characters.
#[arg(long, value_enum, default_value_t, value_name("FORMAT"))]
color_scheme: ColorScheme,
/// Whether to display the position panel on the left.
#[arg(short('P'), long)]
no_position: bool,
@@ -429,6 +433,8 @@ fn run() -> Result<()> {
let character_table = opt.character_table;
let color_scheme = opt.color_scheme;
let mut stdout = BufWriter::new(io::stdout().lock());
let mut printer = PrinterBuilder::new(&mut stdout)
@@ -442,6 +448,7 @@ fn run() -> Result<()> {
.with_base(base)
.endianness(endianness)
.character_table(character_table)
.color_scheme(color_scheme)
.build();
printer.display_offset(skip_offset + display_offset);
printer.print_all(&mut reader).map_err(|e| anyhow!(e))?;