25 Commits
Author SHA1 Message Date
David Peter 6ecc29b9c8 Remove sponsors 2026-04-30 10:24:05 +02:00
David Peter 8eb6d4771c Bump version to v0.17.0 2026-02-14 13:53:54 +01:00
David Peter 5d05382df8 Update README 2026-02-14 13:50:33 +01:00
David Peter 626f03bfbd Fix #254 2026-02-14 13:49:10 +01:00
n4n5 04d74bae47 feat: handle standard input "-" (#257) 2026-02-14 13:33:23 +01:00
n4n5 96e427cb4a Improve crate && fix CI (#256)
* feat: improve crate

* fix: CI
2026-02-14 13:09:13 +01:00
Filip Czaplicki 7f8db4f113 Add colors to --help (#253)
Thank you!
2026-02-14 13:08:00 +01:00
王品呈 2e2643782d Add option to output result in C include file style (#242) (#246)
* feat: Implement "-i/-include" option

* make clippy happy

Suppress clippy warning for too many arguments in Printer struct

* add tests & use buffered reading for include mode to handle large files

* Update CHANGELOG
2025-10-19 21:38:21 +02:00
David Peter 269860ba7e Update README 2025-09-23 19:38:25 +02:00
David Peter 2c8be77181 Update README 2025-09-04 09:00:22 +02:00
David Peter f16e2c96ea Fix CHANGELOG formatting 2025-08-31 21:40:15 +02:00
Friedrich 'friedɀ' SchwedlerandDavid Peter 585d06293a Add shell completion (#251)
* add commandline argument to generate shell completion

* update changelog

* fixup! add commandline argument to generate shell completion

* Update option name and description

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-08-31 21:39:17 +02:00
David Peter 909dd7d947 Update Windows runners 2025-08-31 21:22:59 +02:00
David Peter 5a295296fe Fix formatting 2025-08-31 21:21:22 +02:00
David Peter 38301f3759 Fix clippy, CI 2025-08-31 21:17:54 +02:00
Niclas SchwarzloseandDavid Peter f2547aed69 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>
2025-08-31 21:13:57 +02:00
Dan Hipschman 9ef7c346dd Enable custom colors with environment variables (#248)
* Enable custom colors with environment variables

* Rename HEXYL_XYZ env vars to HEXYL_COLOR_XYZ
2025-06-16 22:41:37 +02:00
Regis Boudinot 6220cae644 Check if terminal_width is less than offset and return 1 (see #244) (#245) 2025-06-08 21:35:42 +02:00
David Peter ad553e9e33 Merge pull request #249 from dan-hipschman/fix-ci
Fix CI
2025-06-08 21:11:18 +02:00
Dan Hipschman fbbd5e3733 Fix CI 2025-05-13 15:18:02 -07:00
David Peter 742c2fcbfd Update README 2025-03-09 19:29:26 +01:00
David Peter 79f7076b02 Merge pull request #239 from ilmanzo/issue238
Fix issue #238
2025-03-02 19:28:12 +01:00
David Peter cba09bf7b7 Merge pull request #241 from saulvaldelvira/master
Fix some clippy warnings
2025-02-19 21:39:49 +01:00
Saúl Valdelvira 06ff4b750e Fix some clippy warnings
Changes:
- Elid lifetimes when possible
- Use a slice instead of a Vec as function argument
- Rewrite &[b'*''] to b"*"
2025-02-19 12:44:08 +01:00
Andrea Manzini 630d92fb8f Fix issue #238 2025-01-26 18:54:13 +01:00
12 changed files with 811 additions and 206 deletions

No files matched your search

+15 -14
View File
@@ -36,7 +36,7 @@ jobs:
ensure_cargo_fmt: ensure_cargo_fmt:
name: Ensure 'cargo fmt' has been run name: Ensure 'cargo fmt' has been run
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
steps: steps:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
@@ -46,7 +46,7 @@ jobs:
min_version: min_version:
name: Minimum supported rust version name: Minimum supported rust version
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
needs: crate_metadata needs: crate_metadata
steps: steps:
- name: Checkout source code - name: Checkout source code
@@ -70,18 +70,19 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
job: job:
- { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - { target: aarch64-unknown-linux-gnu , os: ubuntu-24.04, use-cross: true }
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } - { target: arm-unknown-linux-gnueabihf , os: ubuntu-24.04, use-cross: true }
- { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - { target: arm-unknown-linux-musleabihf, os: ubuntu-24.04, use-cross: true }
- { target: i686-pc-windows-msvc , os: windows-2019 } - { target: i686-pc-windows-msvc , os: windows-2025 }
- { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - { target: i686-unknown-linux-gnu , os: ubuntu-24.04, use-cross: true }
- { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - { target: i686-unknown-linux-musl , os: ubuntu-24.04, use-cross: true }
- { target: x86_64-apple-darwin , os: macos-13 } - { target: x86_64-apple-darwin , os: macos-15-intel }
- { target: aarch64-apple-darwin , os: macos-15 } - { target: aarch64-apple-darwin , os: macos-15 }
- { target: x86_64-pc-windows-gnu , os: windows-2019 } # Was causing CI failures unrelated to app logic
- { target: x86_64-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 }
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - { target: x86_64-pc-windows-msvc , os: windows-2025 }
- { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - { target: x86_64-unknown-linux-gnu , os: ubuntu-24.04, use-cross: true }
- { target: x86_64-unknown-linux-musl , os: ubuntu-24.04, use-cross: true }
env: env:
BUILD_CMD: cargo BUILD_CMD: cargo
steps: steps:
@@ -155,7 +156,7 @@ jobs:
run: | run: |
# test only library unit tests and binary for arm-type targets # test only library unit tests and binary for arm-type targets
unset CARGO_TEST_OPTIONS unset CARGO_TEST_OPTIONS
unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--lib --bin ${{ needs.crate_metadata.outputs.name }}" ;; esac; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--lib --bin ${{ needs.crate_metadata.outputs.name }}" ;; esac;
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
- name: Run tests - name: Run tests
+9 -1
View File
@@ -1,9 +1,17 @@
# unreleased # v0.17.0
## Features ## Features
- Add option to output result in C include file style, see #242 (@wpcwzy)
- Add `--color-scheme` option, see #247 (@aticu)
- Add `braille` character table, see #247 (@aticu)
- Add command line argument to generate shell completion, see #155 (@friedz)
- Add colors to `--help`/`-h`, see #253 (@starsep)
## Bugfixes ## Bugfixes
- Fix memory allocation bug when terminal width is less than 10, see #244 (@selfup)
# v0.16.0 # v0.16.0
Generated
+14 -11
View File
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
@@ -68,13 +68,12 @@ checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
[[package]] [[package]]
name = "assert_cmd" name = "assert_cmd"
version = "2.0.16" version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"bstr", "bstr",
"doc-comment",
"libc", "libc",
"predicates", "predicates",
"predicates-core", "predicates-core",
@@ -128,6 +127,15 @@ dependencies = [
"terminal_size", "terminal_size",
] ]
[[package]]
name = "clap_complete"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
dependencies = [
"clap",
]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.18" version = "4.5.18"
@@ -184,12 +192,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.9" version = "0.3.9"
@@ -217,11 +219,12 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hexyl" name = "hexyl"
version = "0.16.0" version = "0.17.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
"clap", "clap",
"clap_complete",
"const_format", "const_format",
"libc", "libc",
"owo-colors", "owo-colors",
+5 -3
View File
@@ -1,15 +1,16 @@
[package] [package]
authors = ["David Peter <mail@david-peter.de>"] authors = ["David Peter <mail@david-peter.de>"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
keywords = ["hex", "viewer"]
description = "A command-line hex viewer" description = "A command-line hex viewer"
homepage = "https://github.com/sharkdp/hexyl" homepage = "https://github.com/sharkdp/hexyl"
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
name = "hexyl" name = "hexyl"
readme = "README.md" readme = "README.md"
repository = "https://github.com/sharkdp/hexyl" repository = "https://github.com/sharkdp/hexyl"
version = "0.16.0" version = "0.17.0"
edition = "2021" edition = "2021"
rust-version = "1.74" rust-version = "1.88"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
@@ -19,13 +20,14 @@ owo-colors = "4"
supports-color = "3" supports-color = "3"
thiserror = "1.0" thiserror = "1.0"
terminal_size = "0.4" terminal_size = "0.4"
clap_complete = "4"
[dependencies.clap] [dependencies.clap]
version = "4" version = "4"
features = ["derive", "wrap_help"] features = ["derive", "wrap_help"]
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0" assert_cmd = "2.1"
predicates = "3.0" predicates = "3.0"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
+16
View File
@@ -181,6 +181,22 @@ scoop install hexyl
x env use 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 ## License
Licensed under either of Licensed under either of
+16
View File
@@ -106,6 +106,22 @@ _FILE_
**-V**, **\--version** **-V**, **\--version**
: Prints version information. : 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 # NOTES
Source repository: Source repository:
+1 -1
View File
@@ -3,7 +3,7 @@ use std::io;
use hexyl::{BorderStyle, PrinterBuilder}; use hexyl::{BorderStyle, PrinterBuilder};
fn main() { fn main() {
let input = vec![ let input = [
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x44, 0x08, 0x02, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x44, 0x08, 0x02, 0x00, 0x00, 0x00,
]; ];
+110 -8
View File
@@ -1,12 +1,114 @@
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 static COLOR_NULL: LazyLock<String> =
pub const COLOR_OFFSET: &[u8] = colors::BrightBlack::ANSI_FG.as_bytes(); LazyLock::new(|| init_color("NULL", AnsiColors::BrightBlack));
pub const COLOR_ASCII_PRINTABLE: &[u8] = colors::Cyan::ANSI_FG.as_bytes(); pub static COLOR_OFFSET: LazyLock<String> =
pub const COLOR_ASCII_WHITESPACE: &[u8] = colors::Green::ANSI_FG.as_bytes(); LazyLock::new(|| init_color("OFFSET", AnsiColors::BrightBlack));
pub const COLOR_ASCII_OTHER: &[u8] = colors::Green::ANSI_FG.as_bytes(); pub static COLOR_ASCII_PRINTABLE: LazyLock<String> =
pub const COLOR_NONASCII: &[u8] = colors::Yellow::ANSI_FG.as_bytes(); LazyLock::new(|| init_color("ASCII_PRINTABLE", AnsiColors::Cyan));
pub const COLOR_RESET: &[u8] = colors::Default::ANSI_FG.as_bytes(); 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()
}
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] #[rustfmt::skip]
pub const CP437: [char; 256] = [ pub const CP437: [char; 256] = [
+3 -3
View File
@@ -6,7 +6,7 @@ pub enum Input<'a> {
Stdin(io::StdinLock<'a>), Stdin(io::StdinLock<'a>),
} }
impl<'a> Read for Input<'a> { impl Read for Input<'_> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self { match *self {
Input::File(ref mut file) => file.read(buf), Input::File(ref mut file) => file.read(buf),
@@ -15,13 +15,13 @@ impl<'a> Read for Input<'a> {
} }
} }
impl<'a> Seek for Input<'a> { impl Seek for Input<'_> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
fn try_skip<R>(reader: R, pos: SeekFrom, err_desc: &'static str) -> io::Result<u64> fn try_skip<R>(reader: R, pos: SeekFrom, err_desc: &'static str) -> io::Result<u64>
where where
R: Read, R: Read,
{ {
let cant_seek_abs_err = || Err(io::Error::new(io::ErrorKind::Other, err_desc)); let cant_seek_abs_err = || Err(io::Error::other(err_desc));
let offset = match pos { let offset = match pos {
SeekFrom::Current(o) => u64::try_from(o).or_else(|_e| cant_seek_abs_err())?, SeekFrom::Current(o) => u64::try_from(o).or_else(|_e| cant_seek_abs_err())?,
+355 -144
View File
@@ -24,6 +24,13 @@ pub enum ByteCategory {
NonAscii, NonAscii,
} }
pub enum IncludeMode {
File(String), // filename
Stdin,
Slice,
Off,
}
#[derive(Copy, Clone, Debug, Default, ValueEnum)] #[derive(Copy, Clone, Debug, Default, ValueEnum)]
#[non_exhaustive] #[non_exhaustive]
pub enum CharacterTable { pub enum CharacterTable {
@@ -43,6 +50,24 @@ pub enum CharacterTable {
/// Uses code page 437 (for non-ASCII bytes). /// Uses code page 437 (for non-ASCII bytes).
#[value(name = "codepage-437")] #[value(name = "codepage-437")]
CP437, 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)] #[derive(Copy, Clone, Debug, Default, ValueEnum)]
@@ -81,14 +106,29 @@ impl Byte {
} }
} }
fn color(self) -> &'static [u8] { fn color(self, color_scheme: ColorScheme) -> &'static [u8] {
use crate::ByteCategory::*; use crate::ByteCategory::*;
match self.category() { match color_scheme {
Null => COLOR_NULL, ColorScheme::Default => match self.category() {
AsciiPrintable => COLOR_ASCII_PRINTABLE, Null => COLOR_NULL.as_bytes(),
AsciiWhitespace => COLOR_ASCII_WHITESPACE, AsciiPrintable => COLOR_ASCII_PRINTABLE.as_bytes(),
AsciiOther => COLOR_ASCII_OTHER, AsciiWhitespace => COLOR_ASCII_WHITESPACE.as_bytes(),
NonAscii => COLOR_NONASCII, 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 +153,37 @@ impl Byte {
}, },
CharacterTable::CP1047 => CP1047[self.0 as usize], CharacterTable::CP1047 => CP1047[self.0 as usize],
CharacterTable::CP437 => CP437[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 +274,8 @@ pub struct PrinterBuilder<'a, Writer: Write> {
base: Base, base: Base,
endianness: Endianness, endianness: Endianness,
character_table: CharacterTable, character_table: CharacterTable,
include_mode: IncludeMode,
color_scheme: ColorScheme,
} }
impl<'a, Writer: Write> PrinterBuilder<'a, Writer> { impl<'a, Writer: Write> PrinterBuilder<'a, Writer> {
@@ -219,6 +292,8 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> {
base: Base::Hexadecimal, base: Base::Hexadecimal,
endianness: Endianness::Big, endianness: Endianness::Big,
character_table: CharacterTable::Default, character_table: CharacterTable::Default,
include_mode: IncludeMode::Off,
color_scheme: ColorScheme::Default,
} }
} }
@@ -272,20 +347,57 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> {
self self
} }
pub fn include_mode(mut self, include: IncludeMode) -> Self {
self.include_mode = include;
self
}
pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
self.color_scheme = color_scheme;
self
}
pub fn build(self) -> Printer<'a, Writer> { pub fn build(self) -> Printer<'a, Writer> {
Printer::new( Printer {
self.writer, idx: 0,
self.show_color, line_buf: vec![0x0; 8 * self.panels as usize],
self.show_char_panel, writer: self.writer,
self.show_position_panel, show_char_panel: self.show_char_panel,
self.border_style, show_position_panel: self.show_position_panel,
self.use_squeeze, show_color: self.show_color,
self.panels, curr_color: None,
self.group_size, color_scheme: self.color_scheme,
self.base, border_style: self.border_style,
self.endianness, byte_hex_panel: (0u8..=u8::MAX)
self.character_table, .map(|i| match self.base {
) Base::Binary => format!("{i:08b}"),
Base::Octal => format!("{i:03o}"),
Base::Decimal => format!("{i:03}"),
Base::Hexadecimal => format!("{i:02x}"),
})
.collect(),
byte_char_panel: (0u8..=u8::MAX)
.map(|i| format!("{}", Byte(i).as_char(self.character_table)))
.collect(),
byte_hex_panel_g: (0u8..=u8::MAX).map(|i| format!("{i:02x}")).collect(),
squeezer: if self.use_squeeze {
Squeezer::Ignore
} else {
Squeezer::Disabled
},
display_offset: 0,
panels: self.panels,
squeeze_byte: 0x00,
group_size: self.group_size,
base_digits: match self.base {
Base::Binary => 8,
Base::Octal => 3,
Base::Decimal => 3,
Base::Hexadecimal => 2,
},
endianness: self.endianness,
include_mode: self.include_mode,
}
} }
} }
@@ -298,6 +410,7 @@ pub struct Printer<'a, Writer: Write> {
show_position_panel: bool, show_position_panel: bool,
show_color: bool, show_color: bool,
curr_color: Option<&'static [u8]>, curr_color: Option<&'static [u8]>,
color_scheme: ColorScheme,
border_style: BorderStyle, border_style: BorderStyle,
byte_hex_panel: Vec<String>, byte_hex_panel: Vec<String>,
byte_char_panel: Vec<String>, byte_char_panel: Vec<String>,
@@ -314,62 +427,11 @@ pub struct Printer<'a, Writer: Write> {
base_digits: u8, base_digits: u8,
/// Whether to show groups in little or big endian format. /// Whether to show groups in little or big endian format.
endianness: Endianness, endianness: Endianness,
/// Whether to output in C include file style.
include_mode: IncludeMode,
} }
impl<'a, Writer: Write> Printer<'a, Writer> { impl<'a, Writer: Write> Printer<'a, Writer> {
fn new(
writer: &'a mut Writer,
show_color: bool,
show_char_panel: bool,
show_position_panel: bool,
border_style: BorderStyle,
use_squeeze: bool,
panels: u64,
group_size: u8,
base: Base,
endianness: Endianness,
character_table: CharacterTable,
) -> Printer<'a, Writer> {
Printer {
idx: 0,
line_buf: vec![0x0; 8 * panels as usize],
writer,
show_char_panel,
show_position_panel,
show_color,
curr_color: None,
border_style,
byte_hex_panel: (0u8..=u8::MAX)
.map(|i| match base {
Base::Binary => format!("{i:08b}"),
Base::Octal => format!("{i:03o}"),
Base::Decimal => format!("{i:03}"),
Base::Hexadecimal => format!("{i:02x}"),
})
.collect(),
byte_char_panel: (0u8..=u8::MAX)
.map(|i| format!("{}", Byte(i).as_char(character_table)))
.collect(),
byte_hex_panel_g: (0u8..=u8::MAX).map(|i| format!("{i:02x}")).collect(),
squeezer: if use_squeeze {
Squeezer::Ignore
} else {
Squeezer::Disabled
},
display_offset: 0,
panels,
squeeze_byte: 0x00,
group_size,
base_digits: match base {
Base::Binary => 8,
Base::Octal => 3,
Base::Decimal => 3,
Base::Hexadecimal => 2,
},
endianness,
}
}
pub fn display_offset(&mut self, display_offset: u64) -> &mut Self { pub fn display_offset(&mut self, display_offset: u64) -> &mut Self {
self.display_offset = display_offset; self.display_offset = display_offset;
self self
@@ -440,14 +502,14 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
.as_bytes(), .as_bytes(),
)?; )?;
if self.show_color { if self.show_color {
self.writer.write_all(COLOR_OFFSET)?; self.writer.write_all(COLOR_OFFSET.as_bytes())?;
} }
if self.show_position_panel { if self.show_position_panel {
match self.squeezer { match self.squeezer {
Squeezer::Print => { Squeezer::Print => {
self.writer.write_all(&[b'*'])?; self.writer.write_all(b"*")?;
if self.show_color { if self.show_color {
self.writer.write_all(COLOR_RESET)?; self.writer.write_all(COLOR_RESET.as_bytes())?;
} }
self.writer.write_all(b" ")?; self.writer.write_all(b" ")?;
} }
@@ -462,7 +524,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
.write_all(self.byte_hex_panel_g[byte as usize].as_bytes())?; .write_all(self.byte_hex_panel_g[byte as usize].as_bytes())?;
} }
if self.show_color { if self.show_color {
self.writer.write_all(COLOR_RESET)?; self.writer.write_all(COLOR_RESET.as_bytes())?;
} }
} }
} }
@@ -481,9 +543,10 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
Squeezer::Print | Squeezer::Delete => self.writer.write_all(b" ")?, Squeezer::Print | Squeezer::Delete => self.writer.write_all(b" ")?,
Squeezer::Ignore | Squeezer::Disabled => { Squeezer::Ignore | Squeezer::Disabled => {
if let Some(&b) = self.line_buf.get(i as usize) { if let Some(&b) = self.line_buf.get(i as usize) {
if self.show_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.curr_color = Some(Byte(b).color()); self.writer.write_all(Byte(b).color(self.color_scheme))?;
self.curr_color = Some(Byte(b).color(self.color_scheme));
} }
self.writer self.writer
.write_all(self.byte_char_panel[b as usize].as_bytes())?; .write_all(self.byte_char_panel[b as usize].as_bytes())?;
@@ -494,7 +557,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
} }
if i == 8 * self.panels - 1 { if i == 8 * self.panels - 1 {
if self.show_color { if self.show_color {
self.writer.write_all(COLOR_RESET)?; self.writer.write_all(COLOR_RESET.as_bytes())?;
self.curr_color = None; self.curr_color = None;
} }
self.writer.write_all( self.writer.write_all(
@@ -505,7 +568,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
)?; )?;
} else if i % 8 == 7 { } else if i % 8 == 7 {
if self.show_color { if self.show_color {
self.writer.write_all(COLOR_RESET)?; self.writer.write_all(COLOR_RESET.as_bytes())?;
self.curr_color = None; self.curr_color = None;
} }
self.writer.write_all( self.writer.write_all(
@@ -531,14 +594,14 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
Squeezer::Print => { Squeezer::Print => {
if !self.show_position_panel && i == 0 { if !self.show_position_panel && i == 0 {
if self.show_color { if self.show_color {
self.writer.write_all(COLOR_OFFSET)?; self.writer.write_all(COLOR_OFFSET.as_bytes())?;
} }
self.writer self.writer
.write_all(self.byte_char_panel[b'*' as usize].as_bytes())?; .write_all(self.byte_char_panel[b'*' as usize].as_bytes())?;
if self.show_color { 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 { } else if i.is_multiple_of(self.group_size as usize) {
self.writer.write_all(b" ")?; self.writer.write_all(b" ")?;
} }
for _ in 0..self.base_digits { for _ in 0..self.base_digits {
@@ -547,12 +610,12 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
} }
Squeezer::Delete => self.writer.write_all(b" ")?, Squeezer::Delete => self.writer.write_all(b" ")?,
Squeezer::Ignore | Squeezer::Disabled => { Squeezer::Ignore | Squeezer::Disabled => {
if i % (self.group_size as usize) == 0 { if i.is_multiple_of(self.group_size as usize) {
self.writer.write_all(b" ")?; self.writer.write_all(b" ")?;
} }
if self.show_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.writer.write_all(Byte(b).color(self.color_scheme))?;
self.curr_color = Some(Byte(b).color()); self.curr_color = Some(Byte(b).color(self.color_scheme));
} }
self.writer self.writer
.write_all(self.byte_hex_panel[b as usize].as_bytes())?; .write_all(self.byte_hex_panel[b as usize].as_bytes())?;
@@ -562,7 +625,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
if i % 8 == 7 { if i % 8 == 7 {
if self.show_color { if self.show_color {
self.curr_color = None; self.curr_color = None;
self.writer.write_all(COLOR_RESET)?; self.writer.write_all(COLOR_RESET.as_bytes())?;
} }
self.writer.write_all(b" ")?; self.writer.write_all(b" ")?;
// byte is last in last panel // byte is last in last panel
@@ -585,7 +648,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
Ok(()) Ok(())
} }
fn reorder_buffer_to_little_endian(&self, buf: &mut Vec<u8>) { fn reorder_buffer_to_little_endian(&self, buf: &mut [u8]) {
let n = buf.len(); let n = buf.len();
let group_sz = self.group_size as usize; let group_sz = self.group_size as usize;
@@ -617,6 +680,36 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
let mut buf = BufReader::new(reader); let mut buf = BufReader::new(reader);
// special handler for include mode
match &self.include_mode {
// Input from a file
// Output like `unsigned char <filename>[] = { ... }; unsigned int <filename>_len = ...;`
IncludeMode::File(filename) => {
// convert non-alphanumeric characters to '_'
let var_name = filename
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '_' })
.collect::<String>();
writeln!(self.writer, "unsigned char {}[] = {{", var_name)?;
let total_bytes = self.print_bytes_in_include_style(&mut buf)?;
writeln!(self.writer, "}};")?;
writeln!(
self.writer,
"unsigned int {}_len = {};",
var_name, total_bytes
)?;
return Ok(());
}
IncludeMode::Stdin | IncludeMode::Slice => {
self.print_bytes_in_include_style(&mut buf)?;
return Ok(());
}
IncludeMode::Off => {}
}
let leftover = loop { let leftover = loop {
// read a maximum of 8 * self.panels bytes from the reader // read a maximum of 8 * self.panels bytes from the reader
if let Ok(n) = buf.read(&mut self.line_buf) { if let Ok(n) = buf.read(&mut self.line_buf) {
@@ -733,8 +826,8 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
writeln!(self.writer)?; writeln!(self.writer)?;
} else if let Some(n) = leftover { } else if let Some(n) = leftover {
// last line is incomplete // last line is incomplete
self.print_position_panel()?;
self.squeezer = Squeezer::Ignore; self.squeezer = Squeezer::Ignore;
self.print_position_panel()?;
self.print_bytes()?; self.print_bytes()?;
self.squeezer = Squeezer::Print; self.squeezer = Squeezer::Print;
for i in n..8 * self.panels as usize { for i in n..8 * self.panels as usize {
@@ -757,6 +850,44 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
Ok(()) Ok(())
} }
/// Print the bytes in C include file style
/// Return the number of bytes read
fn print_bytes_in_include_style<Reader: Read>(
&mut self,
buf: &mut BufReader<Reader>,
) -> Result<usize, io::Error> {
let mut buffer = [0; 1024];
let mut total_bytes = 0;
let mut is_first_chunk = true;
let mut line_counter = 0;
loop {
match buf.read(&mut buffer) {
Ok(0) => break, // EOF
Ok(bytes_read) => {
total_bytes += bytes_read;
for &byte in &buffer[..bytes_read] {
if line_counter % 12 == 0 {
if !is_first_chunk || line_counter > 0 {
writeln!(self.writer, ",")?;
}
// indentation of first line
write!(self.writer, " ")?;
is_first_chunk = false;
} else {
write!(self.writer, ", ")?;
}
write!(self.writer, "0x{:02x}", byte)?;
line_counter += 1;
}
}
Err(e) => return Err(e),
}
}
writeln!(self.writer)?;
Ok(total_bytes)
}
} }
#[cfg(test)] #[cfg(test)]
@@ -768,19 +899,20 @@ mod tests {
fn assert_print_all_output<Reader: Read>(input: Reader, expected_string: String) { fn assert_print_all_output<Reader: Read>(input: Reader, expected_string: String) {
let mut output = vec![]; let mut output = vec![];
let mut printer = Printer::new( let mut printer = PrinterBuilder::new(&mut output)
&mut output, .show_color(false)
false, .show_char_panel(true)
true, .show_position_panel(true)
true, .with_border_style(BorderStyle::Unicode)
BorderStyle::Unicode, .enable_squeezing(true)
true, .num_panels(2)
2, .group_size(1)
1, .with_base(Base::Hexadecimal)
Base::Hexadecimal, .endianness(Endianness::Big)
Endianness::Big, .character_table(CharacterTable::Default)
CharacterTable::Default, .include_mode(IncludeMode::Off)
); .color_scheme(ColorScheme::Default)
.build();
printer.print_all(input).unwrap(); printer.print_all(input).unwrap();
@@ -824,19 +956,20 @@ mod tests {
.to_owned(); .to_owned();
let mut output = vec![]; let mut output = vec![];
let mut printer: Printer<Vec<u8>> = Printer::new( let mut printer: Printer<Vec<u8>> = PrinterBuilder::new(&mut output)
&mut output, .show_color(false)
false, .show_char_panel(true)
true, .show_position_panel(true)
true, .with_border_style(BorderStyle::Unicode)
BorderStyle::Unicode, .enable_squeezing(true)
true, .num_panels(2)
2, .group_size(1)
1, .with_base(Base::Hexadecimal)
Base::Hexadecimal, .endianness(Endianness::Big)
Endianness::Big, .character_table(CharacterTable::Default)
CharacterTable::Default, .include_mode(IncludeMode::Off)
); .color_scheme(ColorScheme::Default)
.build();
printer.display_offset(0xdeadbeef); printer.display_offset(0xdeadbeef);
printer.print_all(input).unwrap(); printer.print_all(input).unwrap();
@@ -859,19 +992,20 @@ mod tests {
.to_owned(); .to_owned();
let mut output = vec![]; let mut output = vec![];
let mut printer: Printer<Vec<u8>> = Printer::new( let mut printer: Printer<Vec<u8>> = PrinterBuilder::new(&mut output)
&mut output, .show_color(false)
false, .show_char_panel(true)
true, .show_position_panel(true)
true, .with_border_style(BorderStyle::Unicode)
BorderStyle::Unicode, .enable_squeezing(true)
true, .num_panels(4)
4, .group_size(1)
1, .with_base(Base::Hexadecimal)
Base::Hexadecimal, .endianness(Endianness::Big)
Endianness::Big, .character_table(CharacterTable::Default)
CharacterTable::Default, .include_mode(IncludeMode::Off)
); .color_scheme(ColorScheme::Default)
.build();
printer.print_all(input).unwrap(); printer.print_all(input).unwrap();
@@ -920,19 +1054,96 @@ mod tests {
.to_owned(); .to_owned();
let mut output = vec![]; let mut output = vec![];
let mut printer: Printer<Vec<u8>> = Printer::new( let mut printer: Printer<Vec<u8>> = PrinterBuilder::new(&mut output)
&mut output, .show_color(false)
false, .show_char_panel(true)
true, .show_position_panel(true)
true, .with_border_style(BorderStyle::Unicode)
BorderStyle::Unicode, .enable_squeezing(true)
true, .num_panels(3)
3, .group_size(1)
1, .with_base(Base::Hexadecimal)
Base::Hexadecimal, .endianness(Endianness::Big)
Endianness::Big, .character_table(CharacterTable::Default)
CharacterTable::Default, .include_mode(IncludeMode::Off)
); .color_scheme(ColorScheme::Default)
.build();
printer.print_all(input).unwrap();
let actual_string: &str = str::from_utf8(&output).unwrap();
assert_eq!(actual_string, expected_string)
}
// issue#238
#[test]
fn display_offset_in_last_line() {
let input = io::Cursor::new(b"AAAAAAAAAAAAAAAACCCC");
let expected_string = "\
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 41 41 41 41 41 41 41 41 ┊ 41 41 41 41 41 41 41 41 │AAAAAAAA┊AAAAAAAA│
│00000010│ 43 43 43 43 ┊ │CCCC ┊ │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
"
.to_owned();
assert_print_all_output(input, expected_string);
}
#[test]
fn include_mode_from_file() {
let input = io::Cursor::new(b"spamspamspamspamspam");
let expected_string = "unsigned char test_txt[] = {
0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d,
0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d
};
unsigned int test_txt_len = 20;
"
.to_owned();
let mut output = vec![];
let mut printer: Printer<Vec<u8>> = PrinterBuilder::new(&mut output)
.show_color(false)
.show_char_panel(true)
.show_position_panel(true)
.with_border_style(BorderStyle::Unicode)
.enable_squeezing(true)
.num_panels(2)
.group_size(1)
.with_base(Base::Hexadecimal)
.endianness(Endianness::Big)
.character_table(CharacterTable::Default)
.include_mode(IncludeMode::File("test.txt".to_owned()))
.color_scheme(ColorScheme::Default)
.build();
printer.print_all(input).unwrap();
let actual_string: &str = str::from_utf8(&output).unwrap();
assert_eq!(actual_string, expected_string)
}
#[test]
fn include_mode_from_stdin() {
let input = io::Cursor::new(b"spamspamspamspamspam");
let expected_string =
" 0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d,
0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d
"
.to_owned();
let mut output = vec![];
let mut printer: Printer<Vec<u8>> = PrinterBuilder::new(&mut output)
.show_color(false)
.show_char_panel(true)
.show_position_panel(true)
.with_border_style(BorderStyle::Unicode)
.enable_squeezing(true)
.num_panels(2)
.group_size(1)
.with_base(Base::Hexadecimal)
.endianness(Endianness::Big)
.character_table(CharacterTable::Default)
.include_mode(IncludeMode::Stdin)
.color_scheme(ColorScheme::Default)
.build();
printer.print_all(input).unwrap(); printer.print_all(input).unwrap();
+89 -20
View File
@@ -3,8 +3,11 @@ use std::io::{self, prelude::*, BufWriter, SeekFrom};
use std::num::{NonZeroI64, NonZeroU64}; use std::num::{NonZeroI64, NonZeroU64};
use std::path::PathBuf; use std::path::PathBuf;
use clap::builder::styling::{AnsiColor, Effects};
use clap::builder::ArgPredicate; use clap::builder::ArgPredicate;
use clap::{ArgAction, Parser, ValueEnum}; use clap::builder::Styles;
use clap::{ArgAction, CommandFactory, Parser, ValueEnum};
use clap_complete::aot::{generate, Shell};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
@@ -14,7 +17,9 @@ use thiserror::Error as ThisError;
use terminal_size::terminal_size; use terminal_size::terminal_size;
use hexyl::{Base, BorderStyle, CharacterTable, Endianness, Input, PrinterBuilder}; use hexyl::{
Base, BorderStyle, CharacterTable, ColorScheme, Endianness, IncludeMode, Input, PrinterBuilder,
};
use hexyl::{ use hexyl::{
COLOR_ASCII_OTHER, COLOR_ASCII_PRINTABLE, COLOR_ASCII_WHITESPACE, COLOR_NONASCII, COLOR_NULL, COLOR_ASCII_OTHER, COLOR_ASCII_PRINTABLE, COLOR_ASCII_WHITESPACE, COLOR_NONASCII, COLOR_NULL,
@@ -51,8 +56,14 @@ Since the terminal width may not be an evenly divisible by the width per hex dat
the right. the right.
Cannot be used with other width-setting options."; Cannot be used with other width-setting options.";
const STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
.literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
.placeholder(AnsiColor::Cyan.on_default());
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(version, about, max_term_width(90))] #[command(version, about, max_term_width(90), styles = STYLES)]
struct Opt { struct Opt {
/// The file to display. If no FILE argument is given, read from STDIN. /// The file to display. If no FILE argument is given, read from STDIN.
#[arg(value_name("FILE"))] #[arg(value_name("FILE"))]
@@ -129,6 +140,10 @@ struct Opt {
#[arg(long, value_enum, default_value_t, value_name("FORMAT"))] #[arg(long, value_enum, default_value_t, value_name("FORMAT"))]
character_table: CharacterTable, 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. /// Whether to display the position panel on the left.
#[arg(short('P'), long)] #[arg(short('P'), long)]
no_position: bool, no_position: bool,
@@ -188,6 +203,20 @@ struct Opt {
/// Print a table showing how different types of bytes are colored. /// Print a table showing how different types of bytes are colored.
#[arg(long)] #[arg(long)]
print_color_table: bool, print_color_table: bool,
/// Output in C include file style (similar to xxd -i).
#[arg(
short('i'),
long("include"),
help = "Output in C include file style",
conflicts_with("little_endian_format"),
conflicts_with("endianness")
)]
include_mode: bool,
/// Show shell completion for a certain shell
#[arg(long, value_name("SHELL"))]
completion: Option<Shell>,
} }
#[derive(Clone, Debug, Default, ValueEnum)] #[derive(Clone, Debug, Default, ValueEnum)]
@@ -244,16 +273,27 @@ fn run() -> Result<()> {
return print_color_table().map_err(|e| anyhow!(e)); return print_color_table().map_err(|e| anyhow!(e));
} }
if let Some(sh) = opt.completion {
let mut cmd = Opt::command();
let name = cmd.get_name().to_string();
generate(sh, &mut cmd, name, &mut io::stdout());
return Ok(());
}
let stdin = io::stdin(); let stdin = io::stdin();
let mut reader = match opt.file { let mut reader = match &opt.file {
Some(filename) => { Some(filename) => {
if filename.is_dir() { if filename.as_os_str() == "-" {
bail!("'{}' is a directory.", filename.to_string_lossy()); Input::Stdin(stdin.lock())
} } else {
let file = File::open(&filename)?; if filename.is_dir() {
bail!("'{}' is a directory.", filename.to_string_lossy());
}
let file = File::open(filename)?;
Input::File(file) Input::File(file)
}
} }
None => Input::Stdin(stdin.lock()), None => Input::Stdin(stdin.lock()),
}; };
@@ -362,7 +402,7 @@ fn run() -> Result<()> {
} else { } else {
((8 / group_size) * (base_digits * group_size + 1)) + 2 ((8 / group_size) * (base_digits * group_size + 1)) + 2
}; };
if (terminal_width - offset) / col_width < 1 { if (terminal_width.saturating_sub(offset)) / col_width < 1 {
1 1
} else { } else {
(terminal_width - offset) / col_width (terminal_width - offset) / col_width
@@ -429,8 +469,35 @@ fn run() -> Result<()> {
let character_table = opt.character_table; let character_table = opt.character_table;
let color_scheme = opt.color_scheme;
let mut stdout = BufWriter::new(io::stdout().lock()); let mut stdout = BufWriter::new(io::stdout().lock());
let include_mode = match opt.include_mode {
// include mode on
true => {
if let Some(include_file) = opt.file {
// input from a file
if include_file.as_os_str() == "-" {
IncludeMode::File("stdin".to_string())
} else {
IncludeMode::File(
include_file
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("file")
.to_string(),
)
}
} else {
// input from stdin
IncludeMode::Stdin
}
}
// include mode off
false => IncludeMode::Off,
};
let mut printer = PrinterBuilder::new(&mut stdout) let mut printer = PrinterBuilder::new(&mut stdout)
.show_color(show_color) .show_color(show_color)
.show_char_panel(show_char_panel) .show_char_panel(show_char_panel)
@@ -442,6 +509,8 @@ fn run() -> Result<()> {
.with_base(base) .with_base(base)
.endianness(endianness) .endianness(endianness)
.character_table(character_table) .character_table(character_table)
.include_mode(include_mode)
.color_scheme(color_scheme)
.build(); .build();
printer.display_offset(skip_offset + display_offset); printer.display_offset(skip_offset + display_offset);
printer.print_all(&mut reader).map_err(|e| anyhow!(e))?; printer.print_all(&mut reader).map_err(|e| anyhow!(e))?;
@@ -493,32 +562,32 @@ fn print_color_table() -> io::Result<()> {
writeln!(stdout, "hexyl color reference:\n")?; writeln!(stdout, "hexyl color reference:\n")?;
// NULL bytes // NULL bytes
stdout.write_all(COLOR_NULL)?; stdout.write_all(COLOR_NULL.as_bytes())?;
writeln!(stdout, "⋄ NULL bytes (0x00)")?; writeln!(stdout, "⋄ NULL bytes (0x00)")?;
stdout.write_all(COLOR_RESET)?; stdout.write_all(COLOR_RESET.as_bytes())?;
// ASCII printable // ASCII printable
stdout.write_all(COLOR_ASCII_PRINTABLE)?; stdout.write_all(COLOR_ASCII_PRINTABLE.as_bytes())?;
writeln!(stdout, "a ASCII printable characters (0x20 - 0x7E)")?; writeln!(stdout, "a ASCII printable characters (0x20 - 0x7E)")?;
stdout.write_all(COLOR_RESET)?; stdout.write_all(COLOR_RESET.as_bytes())?;
// ASCII whitespace // ASCII whitespace
stdout.write_all(COLOR_ASCII_WHITESPACE)?; stdout.write_all(COLOR_ASCII_WHITESPACE.as_bytes())?;
writeln!(stdout, "_ ASCII whitespace (0x09 - 0x0D, 0x20)")?; writeln!(stdout, "_ ASCII whitespace (0x09 - 0x0D, 0x20)")?;
stdout.write_all(COLOR_RESET)?; stdout.write_all(COLOR_RESET.as_bytes())?;
// ASCII other // ASCII other
stdout.write_all(COLOR_ASCII_OTHER)?; stdout.write_all(COLOR_ASCII_OTHER.as_bytes())?;
writeln!( writeln!(
stdout, stdout,
"• ASCII control characters (except NULL and whitespace)" "• ASCII control characters (except NULL and whitespace)"
)?; )?;
stdout.write_all(COLOR_RESET)?; stdout.write_all(COLOR_RESET.as_bytes())?;
// Non-ASCII // Non-ASCII
stdout.write_all(COLOR_NONASCII)?; stdout.write_all(COLOR_NONASCII.as_bytes())?;
writeln!(stdout, "× Non-ASCII bytes (0x80 - 0xFF)")?; writeln!(stdout, "× Non-ASCII bytes (0x80 - 0xFF)")?;
stdout.write_all(COLOR_RESET)?; stdout.write_all(COLOR_RESET.as_bytes())?;
Ok(()) Ok(())
} }
+178 -1
View File
@@ -1,7 +1,7 @@
use assert_cmd::Command; use assert_cmd::Command;
fn hexyl() -> Command { fn hexyl() -> Command {
let mut cmd = Command::cargo_bin("hexyl").unwrap(); let mut cmd = Command::new(assert_cmd::cargo_bin!("hexyl"));
cmd.current_dir("tests/examples"); cmd.current_dir("tests/examples");
cmd cmd
} }
@@ -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);
}
}