diff --git a/CHANGELOG.md b/CHANGELOG.md index 9241c68..9cc0679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ## Features +- Add `--color-scheme` option, see #247 (@aticu) +- Add `braille` character table, see #247 (@aticu) + ## Bugfixes diff --git a/src/colors.rs b/src/colors.rs index a56e0b0..a16b62c 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -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(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 diff --git a/src/lib.rs b/src/lib.rs index 6d5f569..dd014d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, byte_char_panel: Vec, @@ -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(); diff --git a/src/main.rs b/src/main.rs index 3ef7c05..ef9227e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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))?;