Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package hexyl for openSUSE:Factory checked in at 2026-02-16 13:10:55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/hexyl (Old) and /work/SRC/openSUSE:Factory/.hexyl.new.1977 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "hexyl" Mon Feb 16 13:10:55 2026 rev:9 rq:1333171 version:0.17.0 Changes: -------- --- /work/SRC/openSUSE:Factory/hexyl/hexyl.changes 2025-01-27 20:55:04.330183793 +0100 +++ /work/SRC/openSUSE:Factory/.hexyl.new.1977/hexyl.changes 2026-02-16 13:17:24.146223752 +0100 @@ -1,0 +2,16 @@ +Sat Feb 14 15:14:11 UTC 2026 - Martin Hauke <[email protected]> + +- Update to version 0.17.0 + * Enable custom colors with environment variables. + * Add new braille character table and color schemes. + * Add shell completion. + * Add option to output result in C include file style. + * Fix some clippy warnings. + * Fix issue #238. + * Check if terminal_width is less than offset and return 1 (#244) + * Add colors to --help. + * feat: handle standard input "-". +- Package bash-completion. +- Package manpage. + +------------------------------------------------------------------- Old: ---- cargo_config hexyl-0.16.0.tar.gz New: ---- hexyl-0.17.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ hexyl.spec ++++++ --- /var/tmp/diff_new_pack.j56lRW/_old 2026-02-16 13:17:25.154266791 +0100 +++ /var/tmp/diff_new_pack.j56lRW/_new 2026-02-16 13:17:25.154266791 +0100 @@ -1,8 +1,8 @@ # # spec file for package hexyl # -# Copyright (c) 2025 SUSE LLC -# Copyright (c) 2022-2024, Martin Hauke <[email protected]> +# Copyright (c) 2026 SUSE LLC and contributors +# Copyright (c) 2022-2026, Martin Hauke <[email protected]> # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,19 +18,17 @@ Name: hexyl -Version: 0.16.0 +Version: 0.17.0 Release: 0 Summary: A command-line hex viewer -License: Apache-2.0 +License: Apache-2.0 OR MIT Group: Development/Tools/Other #Git-Clone: https://github.com/sharkdp/hexyl.git URL: https://github.com/sharkdp/hexyl Source: https://github.com/sharkdp/hexyl/archive/refs/tags/v%{version}.tar.gz#/%{name}-%{version}.tar.gz Source1: vendor.tar.xz -Source2: cargo_config -BuildRequires: cargo BuildRequires: cargo-packaging -BuildRequires: rust +BuildRequires: pandoc %description hexyl is a simple hex viewer for the terminal. It uses a colored output @@ -40,19 +38,24 @@ %prep %autosetup -p 1 -a 1 -install -D -m 0644 %{SOURCE2} .cargo/config %build %{cargo_build} +pandoc --standalone --to man ./doc/hexyl.1.md -o hexyl.1 %check %{cargo_test} %install cargo install --no-track --root=%{buildroot}%{_prefix} --path . +install -Dm 0644 %{name}.1 %{buildroot}%{_mandir}/man1/%{name}.1 +%{buildroot}%{_bindir}/%{name} --completion bash > %{name}.bash +install -Dm 0644 %{name}.bash %{buildroot}%{_datadir}/bash-completion/completions/%{name} %files -%license LICENSE-APACHE +%license LICENSE-APACHE LICENSE-MIT %doc CHANGELOG.md README.md %{_bindir}/hexyl +%{_datadir}/bash-completion/completions/%{name} +%{_mandir}/man1/%{name}.1%{?ext_man} ++++++ hexyl-0.16.0.tar.gz -> hexyl-0.17.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/.github/workflows/CICD.yml new/hexyl-0.17.0/.github/workflows/CICD.yml --- old/hexyl-0.16.0/.github/workflows/CICD.yml 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/.github/workflows/CICD.yml 2026-02-14 13:53:54.000000000 +0100 @@ -36,7 +36,7 @@ ensure_cargo_fmt: name: Ensure 'cargo fmt' has been run - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: dtolnay/rust-toolchain@stable with: @@ -46,7 +46,7 @@ min_version: name: Minimum supported rust version - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: crate_metadata steps: - name: Checkout source code @@ -70,18 +70,19 @@ fail-fast: false matrix: job: - - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } - - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - - { target: i686-pc-windows-msvc , os: windows-2019 } - - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - - { target: x86_64-apple-darwin , os: macos-13 } + - { target: aarch64-unknown-linux-gnu , os: ubuntu-24.04, use-cross: true } + - { target: arm-unknown-linux-gnueabihf , os: ubuntu-24.04, use-cross: true } + - { target: arm-unknown-linux-musleabihf, os: ubuntu-24.04, use-cross: true } + - { target: i686-pc-windows-msvc , os: windows-2025 } + - { target: i686-unknown-linux-gnu , os: ubuntu-24.04, use-cross: true } + - { target: i686-unknown-linux-musl , os: ubuntu-24.04, use-cross: true } + - { target: x86_64-apple-darwin , os: macos-15-intel } - { target: aarch64-apple-darwin , os: macos-15 } - - { target: x86_64-pc-windows-gnu , os: windows-2019 } - - { target: x86_64-pc-windows-msvc , os: windows-2019 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + # Was causing CI failures unrelated to app logic + # - { target: x86_64-pc-windows-gnu , os: windows-2019 } + - { target: x86_64-pc-windows-msvc , os: windows-2025 } + - { 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: BUILD_CMD: cargo steps: @@ -155,7 +156,7 @@ run: | # test only library unit tests and binary for arm-type targets 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 - name: Run tests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/CHANGELOG.md new/hexyl-0.17.0/CHANGELOG.md --- old/hexyl-0.16.0/CHANGELOG.md 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/CHANGELOG.md 2026-02-14 13:53:54.000000000 +0100 @@ -1,9 +1,17 @@ -# unreleased +# v0.17.0 ## 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 +- Fix memory allocation bug when terminal width is less than 10, see #244 (@selfup) + # v0.16.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/Cargo.lock new/hexyl-0.17.0/Cargo.lock --- old/hexyl-0.16.0/Cargo.lock 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/Cargo.lock 2026-02-14 13:53:54.000000000 +0100 @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -68,13 +68,12 @@ [[package]] name = "assert_cmd" -version = "2.0.16" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" dependencies = [ "anstyle", "bstr", - "doc-comment", "libc", "predicates", "predicates-core", @@ -129,6 +128,15 @@ ] [[package]] +name = "clap_complete" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a" +dependencies = [ + "clap", +] + +[[package]] name = "clap_derive" version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -185,12 +193,6 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -217,11 +219,12 @@ [[package]] name = "hexyl" -version = "0.16.0" +version = "0.17.0" dependencies = [ "anyhow", "assert_cmd", "clap", + "clap_complete", "const_format", "libc", "owo-colors", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/Cargo.toml new/hexyl-0.17.0/Cargo.toml --- old/hexyl-0.16.0/Cargo.toml 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/Cargo.toml 2026-02-14 13:53:54.000000000 +0100 @@ -1,15 +1,16 @@ [package] authors = ["David Peter <[email protected]>"] categories = ["command-line-utilities"] +keywords = ["hex", "viewer"] description = "A command-line hex viewer" homepage = "https://github.com/sharkdp/hexyl" license = "MIT/Apache-2.0" name = "hexyl" readme = "README.md" repository = "https://github.com/sharkdp/hexyl" -version = "0.16.0" +version = "0.17.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.88" [dependencies] anyhow = "1.0" @@ -19,13 +20,14 @@ supports-color = "3" thiserror = "1.0" terminal_size = "0.4" +clap_complete = "4" [dependencies.clap] version = "4" features = ["derive", "wrap_help"] [dev-dependencies] -assert_cmd = "2.0" +assert_cmd = "2.1" predicates = "3.0" pretty_assertions = "1.4.0" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/README.md new/hexyl-0.17.0/README.md --- old/hexyl-0.16.0/README.md 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/README.md 2026-02-14 13:53:54.000000000 +0100 @@ -7,6 +7,18 @@ `hexyl` is a hex viewer for the terminal. It uses a colored output to distinguish different categories of bytes (NULL bytes, printable ASCII characters, ASCII whitespace characters, other ASCII characters and non-ASCII). +### Sponsors + +A special *thank you* goes to our biggest <a href="doc/sponsors.md">sponsor</a>:<br> + +<a href="https://www.warp.dev/hexyl"> + <img src="doc/sponsors/warp-logo.png" width="200" alt="Warp"> + <br> + <strong>Warp, the intelligent terminal</strong> + <br> + <sub>Available on MacOS, Linux, Windows</sub> +</a> + ## Preview  @@ -181,6 +193,22 @@ 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/doc/hexyl.1.md new/hexyl-0.17.0/doc/hexyl.1.md --- old/hexyl-0.16.0/doc/hexyl.1.md 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/doc/hexyl.1.md 2026-02-14 13:53:54.000000000 +0100 @@ -106,6 +106,22 @@ **-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: Binary files old/hexyl-0.16.0/doc/sponsors/tuple-logo.png and new/hexyl-0.17.0/doc/sponsors/tuple-logo.png differ Binary files old/hexyl-0.16.0/doc/sponsors/warp-logo.png and new/hexyl-0.17.0/doc/sponsors/warp-logo.png differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/doc/sponsors.md new/hexyl-0.17.0/doc/sponsors.md --- old/hexyl-0.16.0/doc/sponsors.md 1970-01-01 01:00:00.000000000 +0100 +++ new/hexyl-0.17.0/doc/sponsors.md 2026-02-14 13:53:54.000000000 +0100 @@ -0,0 +1,14 @@ +## Sponsors + +`hexyl` development is sponsored by many individuals and companies. Thank you very much! + +Please note, that being sponsored does not affect the individuality of the `hexyl` +project or affect the maintainers' actions in any way. +We remain impartial and continue to assess pull requests solely on merit - the +features added, bugs solved, and effect on the overall complexity of the code. +No issue will have a different priority based on sponsorship status of the +reporter. + +Contributions from anybody are most welcomed. + +If you want to see our biggest sponsors, check the top of [`README.md`](../README.md#sponsors). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/examples/simple.rs new/hexyl-0.17.0/examples/simple.rs --- old/hexyl-0.16.0/examples/simple.rs 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/examples/simple.rs 2026-02-14 13:53:54.000000000 +0100 @@ -3,7 +3,7 @@ use hexyl::{BorderStyle, PrinterBuilder}; fn main() { - let input = vec![ + let input = [ 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, ]; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/src/colors.rs new/hexyl-0.17.0/src/colors.rs --- old/hexyl-0.16.0/src/colors.rs 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/src/colors.rs 2026-02-14 13:53:54.000000000 +0100 @@ -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 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() +} + +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] = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/src/input.rs new/hexyl-0.17.0/src/input.rs --- old/hexyl-0.16.0/src/input.rs 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/src/input.rs 2026-02-14 13:53:54.000000000 +0100 @@ -6,7 +6,7 @@ Stdin(io::StdinLock<'a>), } -impl<'a> Read for Input<'a> { +impl Read for Input<'_> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { match *self { Input::File(ref mut file) => file.read(buf), @@ -15,13 +15,13 @@ } } -impl<'a> Seek for Input<'a> { +impl Seek for Input<'_> { fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { fn try_skip<R>(reader: R, pos: SeekFrom, err_desc: &'static str) -> io::Result<u64> where 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 { SeekFrom::Current(o) => u64::try_from(o).or_else(|_e| cant_seek_abs_err())?, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/src/lib.rs new/hexyl-0.17.0/src/lib.rs --- old/hexyl-0.16.0/src/lib.rs 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/src/lib.rs 2026-02-14 13:53:54.000000000 +0100 @@ -24,6 +24,13 @@ NonAscii, } +pub enum IncludeMode { + File(String), // filename + Stdin, + Slice, + Off, +} + #[derive(Copy, Clone, Debug, Default, ValueEnum)] #[non_exhaustive] pub enum CharacterTable { @@ -43,6 +50,24 @@ /// 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 +106,29 @@ } } - fn color(self) -> &'static [u8] { + fn color(self, color_scheme: ColorScheme) -> &'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, + 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 +153,37 @@ }, 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 +274,8 @@ base: Base, endianness: Endianness, character_table: CharacterTable, + include_mode: IncludeMode, + color_scheme: ColorScheme, } impl<'a, Writer: Write> PrinterBuilder<'a, Writer> { @@ -219,6 +292,8 @@ base: Base::Hexadecimal, endianness: Endianness::Big, character_table: CharacterTable::Default, + include_mode: IncludeMode::Off, + color_scheme: ColorScheme::Default, } } @@ -272,20 +347,57 @@ 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> { - Printer::new( - self.writer, - self.show_color, - self.show_char_panel, - self.show_position_panel, - self.border_style, - self.use_squeeze, - self.panels, - self.group_size, - self.base, - self.endianness, - self.character_table, - ) + Printer { + idx: 0, + line_buf: vec![0x0; 8 * self.panels as usize], + writer: self.writer, + show_char_panel: self.show_char_panel, + show_position_panel: self.show_position_panel, + show_color: self.show_color, + curr_color: None, + color_scheme: self.color_scheme, + border_style: self.border_style, + byte_hex_panel: (0u8..=u8::MAX) + .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 @@ 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>, @@ -314,62 +427,11 @@ base_digits: u8, /// Whether to show groups in little or big endian format. endianness: Endianness, + /// Whether to output in C include file style. + include_mode: IncludeMode, } 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 { self.display_offset = display_offset; self @@ -440,14 +502,14 @@ .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'*'])?; + 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 +524,7 @@ .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())?; } } } @@ -481,9 +543,10 @@ 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())?; @@ -494,7 +557,7 @@ } 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 +568,7 @@ )?; } 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,14 +594,14 @@ 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 { + } else if i.is_multiple_of(self.group_size as usize) { self.writer.write_all(b" ")?; } for _ in 0..self.base_digits { @@ -547,12 +610,12 @@ } Squeezer::Delete => self.writer.write_all(b" ")?, 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" ")?; } - 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())?; @@ -562,7 +625,7 @@ 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 @@ -585,7 +648,7 @@ 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 group_sz = self.group_size as usize; @@ -617,6 +680,36 @@ 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 { // read a maximum of 8 * self.panels bytes from the reader if let Ok(n) = buf.read(&mut self.line_buf) { @@ -733,8 +826,8 @@ writeln!(self.writer)?; } else if let Some(n) = leftover { // last line is incomplete - self.print_position_panel()?; self.squeezer = Squeezer::Ignore; + self.print_position_panel()?; self.print_bytes()?; self.squeezer = Squeezer::Print; for i in n..8 * self.panels as usize { @@ -757,6 +850,44 @@ 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)] @@ -768,19 +899,20 @@ fn assert_print_all_output<Reader: Read>(input: Reader, expected_string: String) { let mut output = vec![]; - let mut printer = Printer::new( - &mut output, - false, - true, - true, - BorderStyle::Unicode, - true, - 2, - 1, - Base::Hexadecimal, - Endianness::Big, - CharacterTable::Default, - ); + let mut printer = 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::Off) + .color_scheme(ColorScheme::Default) + .build(); printer.print_all(input).unwrap(); @@ -824,19 +956,20 @@ .to_owned(); let mut output = vec![]; - let mut printer: Printer<Vec<u8>> = Printer::new( - &mut output, - false, - true, - true, - BorderStyle::Unicode, - true, - 2, - 1, - Base::Hexadecimal, - Endianness::Big, - CharacterTable::Default, - ); + 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::Off) + .color_scheme(ColorScheme::Default) + .build(); printer.display_offset(0xdeadbeef); printer.print_all(input).unwrap(); @@ -859,19 +992,20 @@ .to_owned(); let mut output = vec![]; - let mut printer: Printer<Vec<u8>> = Printer::new( - &mut output, - false, - true, - true, - BorderStyle::Unicode, - true, - 4, - 1, - Base::Hexadecimal, - Endianness::Big, - CharacterTable::Default, - ); + 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(4) + .group_size(1) + .with_base(Base::Hexadecimal) + .endianness(Endianness::Big) + .character_table(CharacterTable::Default) + .include_mode(IncludeMode::Off) + .color_scheme(ColorScheme::Default) + .build(); printer.print_all(input).unwrap(); @@ -920,19 +1054,96 @@ .to_owned(); let mut output = vec![]; - let mut printer: Printer<Vec<u8>> = Printer::new( - &mut output, - false, - true, - true, - BorderStyle::Unicode, - true, - 3, - 1, - Base::Hexadecimal, - Endianness::Big, - CharacterTable::Default, - ); + 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(3) + .group_size(1) + .with_base(Base::Hexadecimal) + .endianness(Endianness::Big) + .character_table(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(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/src/main.rs new/hexyl-0.17.0/src/main.rs --- old/hexyl-0.16.0/src/main.rs 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/src/main.rs 2026-02-14 13:53:54.000000000 +0100 @@ -3,8 +3,11 @@ use std::num::{NonZeroI64, NonZeroU64}; use std::path::PathBuf; +use clap::builder::styling::{AnsiColor, Effects}; 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}; @@ -14,7 +17,9 @@ 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::{ COLOR_ASCII_OTHER, COLOR_ASCII_PRINTABLE, COLOR_ASCII_WHITESPACE, COLOR_NONASCII, COLOR_NULL, @@ -51,8 +56,14 @@ the right. 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)] -#[command(version, about, max_term_width(90))] +#[command(version, about, max_term_width(90), styles = STYLES)] struct Opt { /// The file to display. If no FILE argument is given, read from STDIN. #[arg(value_name("FILE"))] @@ -129,6 +140,10 @@ #[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, @@ -188,6 +203,20 @@ /// Print a table showing how different types of bytes are colored. #[arg(long)] 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)] @@ -244,16 +273,27 @@ 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 mut reader = match opt.file { + let mut reader = match &opt.file { Some(filename) => { - if filename.is_dir() { - bail!("'{}' is a directory.", filename.to_string_lossy()); - } - let file = File::open(&filename)?; + if filename.as_os_str() == "-" { + Input::Stdin(stdin.lock()) + } else { + 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()), }; @@ -362,7 +402,7 @@ } else { ((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 } else { (terminal_width - offset) / col_width @@ -429,8 +469,35 @@ let character_table = opt.character_table; + let color_scheme = opt.color_scheme; + 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) .show_color(show_color) .show_char_panel(show_char_panel) @@ -442,6 +509,8 @@ .with_base(base) .endianness(endianness) .character_table(character_table) + .include_mode(include_mode) + .color_scheme(color_scheme) .build(); printer.display_offset(skip_offset + display_offset); printer.print_all(&mut reader).map_err(|e| anyhow!(e))?; @@ -493,32 +562,32 @@ 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(()) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hexyl-0.16.0/tests/integration_tests.rs new/hexyl-0.17.0/tests/integration_tests.rs --- old/hexyl-0.16.0/tests/integration_tests.rs 2024-12-27 14:43:18.000000000 +0100 +++ new/hexyl-0.17.0/tests/integration_tests.rs 2026-02-14 13:53:54.000000000 +0100 @@ -1,7 +1,7 @@ use assert_cmd::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 } @@ -785,3 +785,180 @@ ); } } + +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); + } +} ++++++ vendor.tar.xz ++++++ /work/SRC/openSUSE:Factory/hexyl/vendor.tar.xz /work/SRC/openSUSE:Factory/.hexyl.new.1977/vendor.tar.xz differ: char 15, line 1
