Full Code of sharkdp/hexyl for AI

master 8eb6d4771ce1 cached
21 files
162.9 KB
47.9k tokens
162 symbols
1 requests
Download .txt
Repository: sharkdp/hexyl
Branch: master
Commit: 8eb6d4771ce1
Files: 21
Total size: 162.9 KB

Directory structure:
gitextract_k919d0ul/

├── .github/
│   └── workflows/
│       └── CICD.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── doc/
│   ├── hexyl.1.md
│   └── sponsors.md
├── examples/
│   └── simple.rs
├── src/
│   ├── colors.rs
│   ├── input.rs
│   ├── lib.rs
│   ├── main.rs
│   └── tests.rs
└── tests/
    ├── examples/
    │   ├── .gitattributes
    │   ├── ascii
    │   ├── empty
    │   └── hello_world_elf64
    └── integration_tests.rs

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/CICD.yml
================================================
name: CICD

env:
  CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
  MSRV_FEATURES: ""

on:
  workflow_dispatch:
  pull_request:
  push:
    branches:
      - master
    tags:
      - '*'

jobs:
  crate_metadata:
    name: Extract crate metadata
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Extract crate information
      id: crate_metadata
      run: |
        cargo metadata --no-deps --format-version 1 | jq -r '"name=" + .packages[0].name' | tee -a $GITHUB_OUTPUT
        cargo metadata --no-deps --format-version 1 | jq -r '"version=" + .packages[0].version' | tee -a $GITHUB_OUTPUT
        cargo metadata --no-deps --format-version 1 | jq -r '"maintainer=" + .packages[0].authors[0]' | tee -a $GITHUB_OUTPUT
        cargo metadata --no-deps --format-version 1 | jq -r '"homepage=" + .packages[0].homepage' | tee -a $GITHUB_OUTPUT
        cargo metadata --no-deps --format-version 1 | jq -r '"msrv=" + .packages[0].rust_version' | tee -a $GITHUB_OUTPUT
    outputs:
      name: ${{ steps.crate_metadata.outputs.name }}
      version: ${{ steps.crate_metadata.outputs.version }}
      maintainer: ${{ steps.crate_metadata.outputs.maintainer }}
      homepage: ${{ steps.crate_metadata.outputs.homepage }}
      msrv: ${{ steps.crate_metadata.outputs.msrv }}

  ensure_cargo_fmt:
    name: Ensure 'cargo fmt' has been run
    runs-on: ubuntu-24.04
    steps:
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: rustfmt
    - uses: actions/checkout@v3
    - run: cargo fmt -- --check

  min_version:
    name: Minimum supported rust version
    runs-on: ubuntu-24.04
    needs: crate_metadata
    steps:
    - name: Checkout source code
      uses: actions/checkout@v3

    - name: Install rust toolchain (v${{ needs.crate_metadata.outputs.msrv }})
      uses: dtolnay/rust-toolchain@master
      with:
        toolchain: ${{ needs.crate_metadata.outputs.msrv }}
        components: clippy
    - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
      run: cargo clippy --locked --all-targets ${{ env.MSRV_FEATURES }}
    - name: Run tests
      run: cargo test --locked ${{ env.MSRV_FEATURES }}

  build:
    name: ${{ matrix.job.target }} (${{ matrix.job.os }})
    runs-on: ${{ matrix.job.os }}
    needs: crate_metadata
    strategy:
      fail-fast: false
      matrix:
        job:
          - { 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                      }
          # 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:
    - name: Checkout source code
      uses: actions/checkout@v3

    - name: Install prerequisites
      shell: bash
      run: |
        case ${{ matrix.job.target }} in
          arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
          aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
        esac

    - name: Install Rust toolchain
      uses: dtolnay/rust-toolchain@stable
      with:
        targets: ${{ matrix.job.target }}

    - name: Install cross
      if: matrix.job.use-cross
      uses: taiki-e/install-action@v2
      with:
        tool: cross

    - name: Overwrite build command env variable
      if: matrix.job.use-cross
      shell: bash
      run: echo "BUILD_CMD=cross" >> $GITHUB_ENV

    - name: Show version information (Rust, cargo, GCC)
      shell: bash
      run: |
        gcc --version || true
        rustup -V
        rustup toolchain list
        rustup default
        cargo -V
        rustc -V

    - name: Build
      shell: bash
      run: $BUILD_CMD build --locked --release --target=${{ matrix.job.target }}

    - name: Run example
      if: ${{ !matrix.job.use-cross }}
      shell: bash
      run: $BUILD_CMD run --release --target=${{ matrix.job.target }} --example=simple

    - name: Set binary name & path
      id: bin
      shell: bash
      run: |
        # Figure out suffix of binary
        EXE_suffix=""
        case ${{ matrix.job.target }} in
          *-pc-windows-*) EXE_suffix=".exe" ;;
        esac;

        # Setup paths
        BIN_NAME="${{ needs.crate_metadata.outputs.name }}${EXE_suffix}"
        BIN_PATH="target/${{ matrix.job.target }}/release/${BIN_NAME}"

        # Let subsequent steps know where to find the binary
        echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT
        echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT

    - name: Set testing options
      id: test-options
      shell: bash
      run: |
        # test only library unit tests and binary for arm-type targets
        unset CARGO_TEST_OPTIONS
        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
      shell: bash
      run: $BUILD_CMD test --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}

    - name: Setup Pandoc
      uses: r-lib/actions/setup-pandoc@v2

    - name: Generate man page
      run: pandoc -s -f markdown -t man -o "doc/${{ needs.crate_metadata.outputs.name }}.1" "doc/${{ needs.crate_metadata.outputs.name }}.1.md"

    - name: Create tarball
      id: package
      shell: bash
      run: |
        PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac;
        PKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-v${{ needs.crate_metadata.outputs.version }}-${{ matrix.job.target }}
        PKG_NAME=${PKG_BASENAME}${PKG_suffix}
        echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT

        PKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/package"
        ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/"
        mkdir -p "${ARCHIVE_DIR}"

        # Binary
        cp "${{ steps.bin.outputs.BIN_PATH }}" "$ARCHIVE_DIR"

        # README, LICENSE and CHANGELOG files
        cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR"

        # Man page
        cp "doc/${{ needs.crate_metadata.outputs.name }}.1" "$ARCHIVE_DIR"

        # base compressed package
        pushd "${PKG_STAGING}/" >/dev/null
        case ${{ matrix.job.target }} in
          *-pc-windows-*) 7z -y a "${PKG_NAME}" "${PKG_BASENAME}"/* | tail -2 ;;
          *) tar czf "${PKG_NAME}" "${PKG_BASENAME}"/* ;;
        esac;
        popd >/dev/null

        # Let subsequent steps know where to find the compressed package
        echo "PKG_PATH=${PKG_STAGING}/${PKG_NAME}" >> $GITHUB_OUTPUT

    - name: Create Debian package
      id: debian-package
      shell: bash
      if: startsWith(matrix.job.os, 'ubuntu')
      run: |
        COPYRIGHT_YEARS="2018 - "$(date "+%Y")
        DPKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/debian-package"
        DPKG_DIR="${DPKG_STAGING}/dpkg"
        mkdir -p "${DPKG_DIR}"

        DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}
        DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl
        case ${{ matrix.job.target }} in *-musl*) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac;
        DPKG_VERSION=${{ needs.crate_metadata.outputs.version }}

        unset DPKG_ARCH
        case ${{ matrix.job.target }} in
          aarch64-*-linux-*) DPKG_ARCH=arm64 ;;
          arm-*-linux-*hf) DPKG_ARCH=armhf ;;
          i686-*-linux-*) DPKG_ARCH=i686 ;;
          x86_64-*-linux-*) DPKG_ARCH=amd64 ;;
          *) DPKG_ARCH=notset ;;
        esac;

        DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
        echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT

        # Binary
        install -Dm755 "${{ steps.bin.outputs.BIN_PATH }}" "${DPKG_DIR}/usr/bin/${{ steps.bin.outputs.BIN_NAME }}"

        # Man page
        install -Dm644 'doc/${{ needs.crate_metadata.outputs.name }}.1' "${DPKG_DIR}/usr/share/man/man1/${{ needs.crate_metadata.outputs.name }}.1"
        gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ needs.crate_metadata.outputs.name }}.1"

        # README and LICENSE
        install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
        install -Dm644 "LICENSE-MIT" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-MIT"
        install -Dm644 "LICENSE-APACHE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-APACHE"
        install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
        gzip -n --best "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"

        cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" <<EOF
        Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
        Upstream-Name: ${{ needs.crate_metadata.outputs.name }}
        Source: ${{ needs.crate_metadata.outputs.homepage }}

        Files: *
        Copyright: ${{ needs.crate_metadata.outputs.maintainer }}
        Copyright: $COPYRIGHT_YEARS ${{ needs.crate_metadata.outputs.maintainer }}
        License: Apache-2.0 or MIT

        License: Apache-2.0
          On Debian systems, the complete text of the Apache-2.0 can be found in the
          file /usr/share/common-licenses/Apache-2.0.

        License: MIT
          Permission is hereby granted, free of charge, to any
          person obtaining a copy of this software and associated
          documentation files (the "Software"), to deal in the
          Software without restriction, including without
          limitation the rights to use, copy, modify, merge,
          publish, distribute, sublicense, and/or sell copies of
          the Software, and to permit persons to whom the Software
          is furnished to do so, subject to the following
          conditions:
          .
          The above copyright notice and this permission notice
          shall be included in all copies or substantial portions
          of the Software.
          .
          THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
          ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
          TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
          PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
          SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
          CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
          OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
          IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
          DEALINGS IN THE SOFTWARE.
        EOF
          chmod 644 "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright"

          # control file
          mkdir -p "${DPKG_DIR}/DEBIAN"
          cat > "${DPKG_DIR}/DEBIAN/control" <<EOF
        Package: ${DPKG_BASENAME}
        Version: ${DPKG_VERSION}
        Section: utils
        Priority: optional
        Maintainer: ${{ needs.crate_metadata.outputs.maintainer }}
        Homepage: ${{ needs.crate_metadata.outputs.homepage }}
        Architecture: ${DPKG_ARCH}
        Provides: ${{ needs.crate_metadata.outputs.name }}
        Conflicts: ${DPKG_CONFLICTS}
        Description: A command-line benchmarking tool
        EOF

        DPKG_PATH="${DPKG_STAGING}/${DPKG_NAME}"
        echo "DPKG_PATH=${DPKG_PATH}" >> $GITHUB_OUTPUT

        # build dpkg
        fakeroot dpkg-deb --build "${DPKG_DIR}" "${DPKG_PATH}"

    - name: "Artifact upload: tarball"
      uses: actions/upload-artifact@main
      with:
        name: ${{ steps.package.outputs.PKG_NAME }}
        path: ${{ steps.package.outputs.PKG_PATH }}

    - name: "Artifact upload: Debian package"
      uses: actions/upload-artifact@main
      if: steps.debian-package.outputs.DPKG_NAME
      with:
        name: ${{ steps.debian-package.outputs.DPKG_NAME }}
        path: ${{ steps.debian-package.outputs.DPKG_PATH }}

    - name: Check for release
      id: is-release
      shell: bash
      run: |
        unset IS_RELEASE ; if [[ $GITHUB_REF =~ ^refs/tags/v[0-9].* ]]; then IS_RELEASE='true' ; fi
        echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT

    - name: Publish archives and packages
      uses: softprops/action-gh-release@v1
      if: steps.is-release.outputs.IS_RELEASE
      with:
        files: |
          ${{ steps.package.outputs.PKG_PATH }}
          ${{ steps.debian-package.outputs.DPKG_PATH }}
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
/target
**/*.rs.bk

# Generated files
hexyl.1


================================================
FILE: CHANGELOG.md
================================================
# 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

## Features

* New `--print-color-table` option, see #229 (@sahinfalcon)

## Bugfixes

- Throw an error when try to view a directory, see #234 (@Integral-Tech)


# v0.15.0

## Features

- Add codepage 1047 for EBCDIC, see #226 (@v1gnesh)

## Other

- Rewrite CLI using the derive API, see #225 (@sorairolake)


# v0.14.0

## Features

* New `--character-table` option, with the ability to use [codepage 437](https://www.azabani.com/2020/11/15/xd.html), see #194 and #195 (@sharifhsn)
* New `--character-table=ascii` option for a ASCII-only character table, see #212 and #36 (@sharkdp)

## Bugfixes

* Show output when doing `hexyl /dev/zero`, see #211 (@sharifhsn)
* Respect NO_COLOR environment variable, see #210 (@sharkdp)


# v0.13.1

## Bugfixes

- Correctly handle discontinuous input (stdin), see #196 and #197 (@sharifhsn)

# v0.13.0

## Features

- Support both little and big Endian dumps using `--endianness={little,big}`, see #189 and #104 (@RinHizakura)

## Changes

- **Breaking**: Changed the meaning of the short flag `-C` to be consistent with `hexdump -C`. Previously, this would *hide* the character panel, but now `-C` *shows* the character panel, in case it has been previously (e.g. in an `alias`) disabled with `--no-characters`, see #187 (@sharkdp)

## `hexyl` as a library

- New `endianness` method for `PrinterBuilder`


# v0.12.0

## Features

- Only show one panel by default if the terminal width is not wide enough for two panels, see #182 (@sharkdp)
- Respect the `NO_COLOR` environment variable, see #179 (@sharifhsn)

## Bugfixes

- Do not fail with an error if `--panels=auto` is used and the output is piped, see #184 (@sharkdp)

## Changes

- Breaking: For `xxd`-compatibility reasons, `--group_bytes` has been renamed to `--group-size` (with an `--groupsize` alias), see #121 (@sharkdp)

## `hexyl` as a library

- Breaking: `num_group_bytes` has been renamed to `group_size`.


# v0.11.0

## Features

- Significantly improved performance, see #173 and #176 (@sharifhsn)
- Added variable panels through the `--panels` and `--terminal-width` flags, see [#13](https://github.com/sharkdp/hexyl/issues/13) and [#164](https://github.com/sharkdp/hexyl/pull/164) (@sharifhsn)
- Added new `--group-bytes`/`-g` option, see #104 and #170 (@RinHizakura)
- Added new `--base B` option (where `B` can be `binary`, `octal`, `decimal` or `hexadecimal`), see #147 and #178 (@sharifhsn)
- Show actual zero bytes as `⋄` in the character panel (previously: `0`), in order not to confuse them with ASCII
  `0` bytes if colors are deactivated. Closes #166 (@sharkdp)

## `hexyl` as a library

- Breaking change: `Printer::new` is deprecated as a part of the public API. Alternatively, you can now construct a `Printer` using the `PrinterBuilder` builder API, see [#168](https://github.com/sharkdp/hexyl/pull/168). (@sharifhsn)

## Other

- More tests for the squeezing feature, see #177 (@mkatychev)

## Thank you

Special thanks go to @sharifhsn, not just for the new features,
bugfixes and performance improvements. But also for many internal
improvements of the code base and other maintenance tasks.


# v0.10.0

## Features

- Added new `--plain`, `--no-characters`, and `--no-position` flags, see #154 (@mkatychev)
- Allow hex numbers and units for `--block-size` argument, see #111 and #144 (@merkrafter)

## Other

- Added a man page, see #151 (@sorairolake)
- Mention ability to specify length in hex, see #143 (@merkrafter)
- `--length` and `--bytes` are now marked as conflicting command-line options, see #152 (@sorairolake)


# v0.9.0

## Changes

- Breaking change (binary): setting the `-o/--display-offset` flag no longer overrides the value set by `--skip` [#115](https://github.com/sharkdp/hexyl/issues/115). The first displayed address is now the sum of the two values - this matches the behaviour of `xxd`.

## Features

- Allow relative and negative byte offsets (e.g. `hexyl --skip=-1block`), see #99 (@ErichDonGubler)
- Added `-l` as another alias for '-n/--length' (`xxd` compatibility), see #121 and #135 (@TheDoctor314)

## Bugfixes

- Argument `--length` silently takes precedence over `--bytes`, see #105
- Print warning on empty content, see #107 and #108
- Disallow block sizes of zero, see #110
- Fix newline appearing in `--version` output, see #131 and #133 (@scimas)

## Other

- Better diagnostic messages, see #98 (@ErichDonGubler)

## Packaging

- `hexyl` is now available on snapstore, see #116 (@purveshpatel511)


# v0.8.0

## Features

- A new `--skip <N>` / `-s <N>` option can be used to skip the first `N` bytes of the input, see #16, #88 (@Tarnadas, @MaxJohansen, @ErichDonGubler)
- The `--length`/`--bytes`/`--skip`/`--display-offset` options can now take units for their value argument, for example:
  ``` bash
  hexyl /dev/random --length=1KiB
  hexyl $(which hexyl) --skip=1MiB --length=10KiB
  ```
  Both decimal SI prefixes (kB, MB, …) as well as binary IEC prefixes (KiB, MiB, …) are supported.
  In addition, there is a new `--block-size <SIZE>` option that can be used to control the size of the `block`
  unit:
  ``` bash
  hexyl /dev/random --block-size=4kB --length=2block
  ```
  See: #44 (@ErichDonGubler and @aswild)

## Other

- Various improvements throughout the code base by @ErichDonGubler

## Packaging

- `hexyl` is now available on Void Linux, see #91 (@notramo)


# v0.7.0

## Bugfixes

- hexyl can now be closed with `Ctrl-C` when reading input from STDIN, see #84

## Changes

- Breaking change (library): [`Printer::print_all`](https://docs.rs/hexyl/latest/hexyl/struct.Printer.html#method.print_all) does not take a second argument anymore.
- Added an example on how to use `hexyl` as a library: https://github.com/sharkdp/hexyl/blob/v0.7.0/examples/simple.rs


# v0.6.0

## Features

- `hexyl` can now be used as a library, see #67 (@tommilligan)

- Added a new `-o`/`--display-offset` option to add a certain offset to the
  reported file positions, see #57 (@tommilligan)

## Bugfixes

- Remove additional space on short input, see #69 (@nalshihabi)

## Other

- Performance improvements, see #73 and #66


# v0.5.1

## Bugfixes

- A bug in the squeezing logic caused a wrong hexdump, see #62 (@awidegreen)
- Some colors are printed even if they're disabled, see #64 (@awidegreen)
- Fixed build failure on OpenBSD 6.5, see #61


# v0.5.0

## Features

- Added support for squeezing where reoccurring lines are squashed together and visualized with an asterisk. A new `-v`/`--no-squeezing` option can be used to disable the feature. For details, see #59 (@awidegreen)
- Added a new `--border` option with support for various styles (Unicode, ASCII, None), see #54 (@dmke)
- The `--length`/`-n` argument can be passed as a hexadecimal number (`hexyl -n 0xff /dev/urandom`), see #45 (@Qyriad)
- Added `--bytes`/`-c` as an alias for `--length`/`-n`, see #48 (@selfup)

## Changes

- Print header immediately before the first line, see #51 (@mziter)


# v0.4.0

## Features

- Added a new `--color=always/auto/never` option which can be used
  to control `hexyl`s color output, see #30 (@bennetthardwick)
- Use 16 colors instead of 256, see #38

## Changes

- Various speed improvements, see #33 (@kballard)

## Bugfixes

- Proper Ctrl-C handling, see #35
- Proper handling of broken pipes (`hexyl … | head`)


# v0.3.1

- Various (huge) performance improvements, see #23 and #24 (@kballard)
- Replaced 24-bit truecolor ANSI codes by 8-bit codes to support
  more terminal emulators, fixes #9


# v0.3.0

Windows support


# v0.2.0

Initial release


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

Thank you for considering to contribute to `hexyl`!



## Add an entry to the changelog

If your contribution changes the behavior of `hexyl` (as opposed to a typo-fix
in the documentation), please update the [`CHANGELOG.md`](CHANGELOG.md) file
and describe your changes. This makes the release process much easier and
therefore helps to get your changes into a new `hexyl` release faster.

The top of the `CHANGELOG` contains a *"unreleased"* section with a few
subsections (Features, Bugfixes, …). Please add your entry to the subsection
that best describes your change

Entries follow this format:
```
- Short description of what has been changed, see #123 (@user)
```
Here, `#123` is the number of the original issue and/or your pull request.
Please replace `@user` by your GitHub username.


## Adding a new feature

Please consider opening a [ticket](https://github.com/sharkdp/hexyl/issues/new)
first in order to give us a chance to discuss the feature first.


================================================
FILE: Cargo.toml
================================================
[package]
authors = ["David Peter <mail@david-peter.de>"]
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.17.0"
edition = "2021"
rust-version = "1.88"

[dependencies]
anyhow = "1.0"
const_format = "0.2"
libc = "0.2"
owo-colors = "4"
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.1"
predicates = "3.0"
pretty_assertions = "1.4.0"

[profile.release]
lto = true
codegen-units = 1


================================================
FILE: LICENSE-APACHE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: LICENSE-MIT
================================================
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
![](doc/logo.svg)

[![CICD](https://github.com/sharkdp/hexyl/actions/workflows/CICD.yml/badge.svg)](https://github.com/sharkdp/hexyl/actions/workflows/CICD.yml)
[![](https://img.shields.io/crates/l/hexyl.svg?colorB=22ba4c)](https://crates.io/crates/hexyl)
![](https://img.shields.io/crates/v/hexyl.svg?colorB=00aa88)

`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

![](https://i.imgur.com/MWO9uSL.png)

![](https://i.imgur.com/Dp7Wncz.png)

![](https://i.imgur.com/ln3TniI.png)

![](https://i.imgur.com/f8nm8g6.png)


## Installation

### On Ubuntu

*... and other Debian-based Linux distributions.*

If you run Ubuntu 19.10 (Eoan Ermine) or newer, you can install the [officially maintained package](https://packages.ubuntu.com/search?keywords=hexyl):
```bash
sudo apt install hexyl
```
If you use an older version of Ubuntu, you can download
the latest `.deb` package from the release page and install it via:

``` bash
sudo dpkg -i hexyl_0.15.0_amd64.deb  # adapt version number and architecture
```

### On Debian

If you run Debian Buster or newer, you can install the [officially maintained Debian package](https://packages.debian.org/search?searchon=names&keywords=hexyl):
```bash
sudo apt-get install hexyl
```

If you run an older version of Debian, see above for instructions on how to
manually install `hexyl`.

### On Fedora

If you run Fedora 35 or newer, you can install the [officially maintained Fedora package](https://packages.fedoraproject.org/pkgs/rust-hexyl/hexyl):

```bash
sudo dnf install hexyl
```

### On Arch Linux

You can install `hexyl` from [the official package repository](https://archlinux.org/packages/extra/x86_64/hexyl/):

```
pacman -S hexyl
```

### On Void Linux

```
xbps-install hexyl
```

### On Gentoo Linux

Available in [dm9pZCAq overlay](https://github.com/gentoo-mirror/dm9pZCAq)

```
sudo eselect repository enable dm9pZCAq
sudo emerge --sync dm9pZCAq
sudo emerge sys-apps/hexyl::dm9pZCAq
```

### On macOS

Via [Homebrew](https://brew.sh):

```
brew install hexyl
```

...or via [MacPorts](https://www.macports.org):

```
sudo port install hexyl
```

### On FreeBSD

```
pkg install hexyl
```

### On NetBSD

```
pkgin install hexyl
```

### On OpenBSD

```
doas pkg_add hexyl
```

### on Termux
```
pkg install hexyl
```
or
```
apt install hexyl
```

### Via Nix

```
nix-env -i hexyl
```

### Via Guix

```
guix package -i hexyl
```

Or add the `hexyl` package in the list of packages to be installed in your system configuration (e.g., `/etc/config.scm`).

### On other distributions

Check out the [release page](https://github.com/sharkdp/hexyl/releases) for binary builds.

### On Windows

Check out the [release page](https://github.com/sharkdp/hexyl/releases) for binary builds.
Alternatively, install from source via `cargo`, `snap` or `scoop` (see below).
Make sure that you use a terminal that supports ANSI escape sequences (like ConHost v2 since Windows 10 1703
or Windows Terminal since Windows 10 1903).

### Via cargo

If you have Rust 1.56 or higher, you can install `hexyl` from source via `cargo`:
```
cargo install hexyl
```

Alternatively, you can install `hexyl` directly from the repository by using:
```
git clone https://github.com/sharkdp/hexyl
cargo install --path ./hexyl
```

Note: To convert the man page, you will need [Pandoc](https://pandoc.org/).

You can convert from Markdown by using (in the project root):
```
pandoc -s -f markdown -t man -o ./doc/hexyl.1 ./doc/hexyl.1.md
```

### Via snap package

```
sudo snap install hexyl
```
[Get it from the Snap Store](https://snapcraft.io/hexyl)


### Via [Scoop](https://scoop.sh)
```
scoop install hexyl
```

### Via [X-CMD](https://x-cmd.com)
```
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

 * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
 * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)

at your option.


================================================
FILE: doc/hexyl.1.md
================================================
% HEXYL(1) hexyl 0.12.0 | General Commands Manual
%
% 2022-12-05

# NAME

hexyl - a command-line hex viewer

# SYNOPSIS

**hexyl** [_OPTIONS_] [_FILE_]

# DESCRIPTION

**hexyl** is a simple 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).

# POSITIONAL ARGUMENTS

_FILE_
:   The file to display.
    If no _FILE_ argument is given, read from STDIN.

# OPTIONS

**-n**, **\--length** _N_
:   Only read _N_ bytes from the input.
    The _N_ argument can also include a unit with a decimal prefix (kB, MB, ..)
    or binary prefix (kiB, MiB, ..), or can be specified using a hex number.

    Examples:

    :   

        Read the first 64 bytes:
        :   $ **hexyl \--length=64**

        Read the first 4 kibibytes:
        :   $ **hexyl \--length=4KiB**

        Read the first 255 bytes (specified using a hex number):
        :   $ **hexyl \--length=0xff**

**-c**, **\--bytes** _N_
:   An alias for **-n**/**\--length**.

**-l** _N_
:   Yet another alias for **-n**/**\--length**.

**-s**, **\--skip** _N_
:   Skip the first _N_ bytes of the input.
    The _N_ argument can also include a unit (see **\--length** for details).
    A negative value is valid and will seek from the end of the file.

**\--block-size** _SIZE_
:   Sets the size of the block unit to _SIZE_ (default is 512).

    Examples:

    :   

        Sets the block size to 1024 bytes:
        :   $ **hexyl \--block-size=1024 \--length=5block**

        Sets the block size to 4 kilobytes:
        :   $ **hexyl \--block-size=4kB \--length=2block**

**-v**, **\--no-squeezing**
:   Displays all input data.
    Otherwise any number of groups of output lines which would be identical to
    the preceding group of lines, are replaced with a line comprised of a
    single asterisk.

**\--color** _WHEN_
:   When to use colors.
    The auto-mode only displays colors if the output goes to an interactive
    terminal.

    Possible values:

    :   - **always** (default)
        - **auto**
        - **never**

**\--border** _STYLE_
:   Whether to draw a border with Unicode characters, ASCII characters, or none
    at all.

    Possible values:

    :   - **unicode** (default)
        - **ascii**
        - **none**

**-o**, **\--display-offset** _N_
:   Add _N_ bytes to the displayed file position.
    The _N_ argument can also include a unit (see **\--length** for details).
    A negative value is valid and calculates an offset relative to the end of
    the file.

**-h**, **\--help**
:   Prints help information.

**-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:
:   <https://github.com/sharkdp/hexyl>

# EXAMPLES

Print a given file:
:   $ **hexyl small.png**

Print and view a given file in the terminal pager:
:   $ **hexyl big.png | less -r**

Print the first 256 bytes of a given special file:
:   $ **hexyl -n 256 /dev/urandom**

# AUTHORS

**hexyl** was written by David Peter <mail@david-peter.de>.

# REPORTING BUGS

Bugs can be reported on GitHub at:
:   <https://github.com/sharkdp/hexyl/issues>

# COPYRIGHT

**hexyl** is dual-licensed under:

:   - Apache License 2.0 (<https://www.apache.org/licenses/LICENSE-2.0>)
    - MIT License (<https://opensource.org/licenses/MIT>)

# SEE ALSO

**hexdump**(1), **xxd**(1)


================================================
FILE: doc/sponsors.md
================================================
## 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).


================================================
FILE: examples/simple.rs
================================================
use std::io;

use hexyl::{BorderStyle, PrinterBuilder};

fn main() {
    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,
    ];

    let stdout = io::stdout();
    let mut handle = stdout.lock();

    let mut printer = PrinterBuilder::new(&mut handle)
        .show_color(true)
        .show_char_panel(true)
        .show_position_panel(true)
        .with_border_style(BorderStyle::Unicode)
        .enable_squeezing(false)
        .num_panels(2)
        .group_size(1)
        .build();
    printer.print_all(&input[..]).unwrap();
}


================================================
FILE: src/colors.rs
================================================
use owo_colors::{colors, AnsiColors, Color, DynColors, OwoColorize};
use std::str::FromStr;
use std::sync::LazyLock;

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] = [
    // Copyright (c) 2016, Delan Azabani <delan@azabani.com>
    //
    // Permission to use, copy, modify, and/or distribute this software for any
    // purpose with or without fee is hereby granted, provided that the above
    // copyright notice and this permission notice appear in all copies.
    //
    // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    //
    // modified to use the ⋄ character instead of ␀

    // use https://en.wikipedia.org/w/index.php?title=Code_page_437&oldid=978947122
    // not ftp://ftp.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/CP437.TXT
    // because we want the graphic versions of 01h–1Fh + 7Fh
    '⋄','☺','☻','♥','♦','♣','♠','•','◘','○','◙','♂','♀','♪','♫','☼',
    '►','◄','↕','‼','¶','§','▬','↨','↑','↓','→','←','∟','↔','▲','▼',
    ' ','!','"','#','$','%','&','\'','(',')','*','+',',','-','.','/',
    '0','1','2','3','4','5','6','7','8','9',':',';','<','=','>','?',
    '@','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O',
    'P','Q','R','S','T','U','V','W','X','Y','Z','[','\\',']','^','_',
    '`','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o',
    'p','q','r','s','t','u','v','w','x','y','z','{','|','}','~','⌂',
    'Ç','ü','é','â','ä','à','å','ç','ê','ë','è','ï','î','ì','Ä','Å',
    'É','æ','Æ','ô','ö','ò','û','ù','ÿ','Ö','Ü','¢','£','¥','₧','ƒ',
    'á','í','ó','ú','ñ','Ñ','ª','º','¿','⌐','¬','½','¼','¡','«','»',
    '░','▒','▓','│','┤','╡','╢','╖','╕','╣','║','╗','╝','╜','╛','┐',
    '└','┴','┬','├','─','┼','╞','╟','╚','╔','╩','╦','╠','═','╬','╧',
    '╨','╤','╥','╙','╘','╒','╓','╫','╪','┘','┌','█','▄','▌','▐','▀',
    'α','ß','Γ','π','Σ','σ','µ','τ','Φ','Θ','Ω','δ','∞','φ','ε','∩',
    '≡','±','≥','≤','⌠','⌡','÷','≈','°','∙','·','√','ⁿ','²','■','ff',
];

#[rustfmt::skip]
pub const CP1047: [char; 256] = [
     //
     //  Copyright (c) 2016,2024 IBM Corporation and other Contributors.
     //
     //  All rights reserved. This program and the accompanying materials
     //  are made available under the terms of the Eclipse Public License v1.0
     //  which accompanies this distribution, and is available at
     //  http://www.eclipse.org/legal/epl-v10.html
     //
     //  Contributors:
     //    Mark Taylor - Initial Contribution
     //

     // ref1 https://github.com/ibm-messaging/mq-smf-csv/blob/master/src/smfConv.c
    //  ref2 https://web.archive.org/web/20150607033635/http://www-01.ibm.com/software/globalization/cp/cp01047.html
    '.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
    '.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
    '.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
    '.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
    ' ','.','.','.','.','.','.','.','.','.','$','.','<','(','+','|',
    '&','.','.','.','.','.','.','.','.','.','!','$','*',')',';','.',
    '-','/','.','.','.','.','.','.','.','.','.',',','%','_','>','?',
    '.','.','.','.','.','.','.','.','.','.',':','#','@','\'','=','.',
    '.','a','b','c','d','e','f','g','h','i','.','{','.','(','+','.',
    '.','j','k','l','m','n','o','p','q','r','.','}','.',')','.','.',
    '.','~','s','t','u','v','w','x','y','z','.','.','.','.','.','.',
    '.','.','.','.','.','.','.','.','.','.','[',']','.','.','.','-',
    '{','A','B','C','D','E','F','G','H','I','.','.','.','.','.','.',
    '}','J','K','L','M','N','O','P','Q','R','.','.','.','.','.','.',
    '.','.','S','T','U','V','W','X','Y','Z','.','.','.','.','.','.',
    '0','1','2','3','4','5','6','7','8','9','.','.','.','.','.','.'
];


================================================
FILE: src/input.rs
================================================
use std::fs;
use std::io::{self, copy, sink, Read, Seek, SeekFrom};

pub enum Input<'a> {
    File(fs::File),
    Stdin(io::StdinLock<'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),
            Input::Stdin(ref mut stdin) => stdin.read(buf),
        }
    }
}

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::other(err_desc));

            let offset = match pos {
                SeekFrom::Current(o) => u64::try_from(o).or_else(|_e| cant_seek_abs_err())?,
                SeekFrom::Start(_) | SeekFrom::End(_) => cant_seek_abs_err()?,
            };

            copy(&mut reader.take(offset), &mut sink())
        }

        match *self {
            Input::File(ref mut file) => {
                let seek_res = file.seek(pos);
                if let Err(Some(libc::ESPIPE)) = seek_res.as_ref().map_err(|err| err.raw_os_error())
                {
                    try_skip(
                        file,
                        pos,
                        "Pipes only support seeking forward with a relative offset",
                    )
                } else {
                    seek_res
                }
            }
            Input::Stdin(ref mut stdin) => try_skip(
                stdin,
                pos,
                "STDIN only supports seeking forward with a relative offset",
            ),
        }
    }
}

impl<'a> Input<'a> {
    pub fn into_inner(self) -> Box<dyn Read + 'a> {
        match self {
            Input::File(file) => Box::new(file),
            Input::Stdin(stdin) => Box::new(stdin),
        }
    }
}


================================================
FILE: src/lib.rs
================================================
pub(crate) mod colors;
pub(crate) mod input;

pub use colors::*;
pub use input::*;

use std::io::{self, BufReader, Read, Write};

use clap::ValueEnum;

pub enum Base {
    Binary,
    Octal,
    Decimal,
    Hexadecimal,
}

#[derive(Copy, Clone)]
pub enum ByteCategory {
    Null,
    AsciiPrintable,
    AsciiWhitespace,
    AsciiOther,
    NonAscii,
}

pub enum IncludeMode {
    File(String), // filename
    Stdin,
    Slice,
    Off,
}

#[derive(Copy, Clone, Debug, Default, ValueEnum)]
#[non_exhaustive]
pub enum CharacterTable {
    /// Show printable ASCII characters as-is, '⋄' for NULL bytes, ' ' for
    /// space, '_' for other ASCII whitespace, '•' for other ASCII characters,
    /// and '×' for non-ASCII bytes.
    #[default]
    Default,

    /// Show printable ASCII as-is, ' ' for space, '.' for everything else.
    Ascii,

    /// Show printable EBCDIC as-is, ' ' for space, '.' for everything else.
    #[value(name = "codepage-1047")]
    CP1047,

    /// 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)]
pub enum Endianness {
    /// Print out groups in little-endian format.
    Little,

    /// Print out groups in big-endian format.
    #[default]
    Big,
}

#[derive(PartialEq)]
enum Squeezer {
    Print,
    Delete,
    Ignore,
    Disabled,
}

#[derive(Copy, Clone)]
struct Byte(u8);

impl Byte {
    fn category(self) -> ByteCategory {
        if self.0 == 0x00 {
            ByteCategory::Null
        } else if self.0.is_ascii_graphic() {
            ByteCategory::AsciiPrintable
        } else if self.0.is_ascii_whitespace() {
            ByteCategory::AsciiWhitespace
        } else if self.0.is_ascii() {
            ByteCategory::AsciiOther
        } else {
            ByteCategory::NonAscii
        }
    }

    fn color(self, color_scheme: ColorScheme) -> &'static [u8] {
        use crate::ByteCategory::*;
        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],
            },
        }
    }

    fn as_char(self, character_table: CharacterTable) -> char {
        use crate::ByteCategory::*;
        match character_table {
            CharacterTable::Default => match self.category() {
                Null => '⋄',
                AsciiPrintable => self.0 as char,
                AsciiWhitespace if self.0 == 0x20 => ' ',
                AsciiWhitespace => '_',
                AsciiOther => '•',
                NonAscii => '×',
            },
            CharacterTable::Ascii => match self.category() {
                Null => '.',
                AsciiPrintable => self.0 as char,
                AsciiWhitespace if self.0 == 0x20 => ' ',
                AsciiWhitespace => '.',
                AsciiOther => '.',
                NonAscii => '.',
            },
            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()
                }
            },
        }
    }
}

struct BorderElements {
    left_corner: char,
    horizontal_line: char,
    column_separator: char,
    right_corner: char,
}

#[derive(Clone, Copy, Debug, Default, ValueEnum)]
pub enum BorderStyle {
    /// Draw a border with Unicode characters.
    #[default]
    Unicode,

    /// Draw a border with ASCII characters.
    Ascii,

    /// Do not draw a border at all.
    None,
}

impl BorderStyle {
    fn header_elems(&self) -> Option<BorderElements> {
        match self {
            BorderStyle::Unicode => Some(BorderElements {
                left_corner: '┌',
                horizontal_line: '─',
                column_separator: '┬',
                right_corner: '┐',
            }),
            BorderStyle::Ascii => Some(BorderElements {
                left_corner: '+',
                horizontal_line: '-',
                column_separator: '+',
                right_corner: '+',
            }),
            BorderStyle::None => None,
        }
    }

    fn footer_elems(&self) -> Option<BorderElements> {
        match self {
            BorderStyle::Unicode => Some(BorderElements {
                left_corner: '└',
                horizontal_line: '─',
                column_separator: '┴',
                right_corner: '┘',
            }),
            BorderStyle::Ascii => Some(BorderElements {
                left_corner: '+',
                horizontal_line: '-',
                column_separator: '+',
                right_corner: '+',
            }),
            BorderStyle::None => None,
        }
    }

    fn outer_sep(&self) -> char {
        match self {
            BorderStyle::Unicode => '│',
            BorderStyle::Ascii => '|',
            BorderStyle::None => ' ',
        }
    }

    fn inner_sep(&self) -> char {
        match self {
            BorderStyle::Unicode => '┊',
            BorderStyle::Ascii => '|',
            BorderStyle::None => ' ',
        }
    }
}

pub struct PrinterBuilder<'a, Writer: Write> {
    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,
    include_mode: IncludeMode,
    color_scheme: ColorScheme,
}

impl<'a, Writer: Write> PrinterBuilder<'a, Writer> {
    pub fn new(writer: &'a mut Writer) -> Self {
        PrinterBuilder {
            writer,
            show_color: true,
            show_char_panel: true,
            show_position_panel: true,
            border_style: BorderStyle::Unicode,
            use_squeeze: true,
            panels: 2,
            group_size: 1,
            base: Base::Hexadecimal,
            endianness: Endianness::Big,
            character_table: CharacterTable::Default,
            include_mode: IncludeMode::Off,
            color_scheme: ColorScheme::Default,
        }
    }

    pub fn show_color(mut self, show_color: bool) -> Self {
        self.show_color = show_color;
        self
    }

    pub fn show_char_panel(mut self, show_char_panel: bool) -> Self {
        self.show_char_panel = show_char_panel;
        self
    }

    pub fn show_position_panel(mut self, show_position_panel: bool) -> Self {
        self.show_position_panel = show_position_panel;
        self
    }

    pub fn with_border_style(mut self, border_style: BorderStyle) -> Self {
        self.border_style = border_style;
        self
    }

    pub fn enable_squeezing(mut self, enable: bool) -> Self {
        self.use_squeeze = enable;
        self
    }

    pub fn num_panels(mut self, num: u64) -> Self {
        self.panels = num;
        self
    }

    pub fn group_size(mut self, num: u8) -> Self {
        self.group_size = num;
        self
    }

    pub fn with_base(mut self, base: Base) -> Self {
        self.base = base;
        self
    }

    pub fn endianness(mut self, endianness: Endianness) -> Self {
        self.endianness = endianness;
        self
    }

    pub fn character_table(mut self, character_table: CharacterTable) -> Self {
        self.character_table = character_table;
        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 {
            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,
        }
    }
}

pub struct Printer<'a, Writer: Write> {
    idx: u64,
    /// the buffer containing all the bytes in a line for character printing
    line_buf: Vec<u8>,
    writer: &'a mut Writer,
    show_char_panel: bool,
    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>,
    // same as previous but in Fixed(242) gray color, for position panel
    byte_hex_panel_g: Vec<String>,
    squeezer: Squeezer,
    display_offset: u64,
    /// The number of panels to draw.
    panels: u64,
    squeeze_byte: usize,
    /// The number of octets per group.
    group_size: u8,
    /// The number of digits used to write the base.
    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> {
    pub fn display_offset(&mut self, display_offset: u64) -> &mut Self {
        self.display_offset = display_offset;
        self
    }

    fn panel_sz(&self) -> usize {
        // add one to include the trailing space of a group
        let group_sz = self.base_digits as usize * self.group_size as usize + 1;
        let group_per_panel = 8 / self.group_size as usize;
        // add one to include the leading space
        1 + group_sz * group_per_panel
    }

    fn write_border(&mut self, border_elements: BorderElements) -> io::Result<()> {
        let h = border_elements.horizontal_line;
        let c = border_elements.column_separator;
        let l = border_elements.left_corner;
        let r = border_elements.right_corner;
        let h8 = h.to_string().repeat(8);
        let h_repeat = h.to_string().repeat(self.panel_sz());

        if self.show_position_panel {
            write!(self.writer, "{l}{h8}{c}")?;
        } else {
            write!(self.writer, "{l}")?;
        }

        for _ in 0..self.panels - 1 {
            write!(self.writer, "{h_repeat}{c}")?;
        }
        if self.show_char_panel {
            write!(self.writer, "{h_repeat}{c}")?;
        } else {
            write!(self.writer, "{h_repeat}")?;
        }

        if self.show_char_panel {
            for _ in 0..self.panels - 1 {
                write!(self.writer, "{h8}{c}")?;
            }
            writeln!(self.writer, "{h8}{r}")?;
        } else {
            writeln!(self.writer, "{r}")?;
        }

        Ok(())
    }

    pub fn print_header(&mut self) -> io::Result<()> {
        if let Some(e) = self.border_style.header_elems() {
            self.write_border(e)?
        }
        Ok(())
    }

    pub fn print_footer(&mut self) -> io::Result<()> {
        if let Some(e) = self.border_style.footer_elems() {
            self.write_border(e)?
        }
        Ok(())
    }

    fn print_position_panel(&mut self) -> io::Result<()> {
        self.writer.write_all(
            self.border_style
                .outer_sep()
                .encode_utf8(&mut [0; 4])
                .as_bytes(),
        )?;
        if self.show_color {
            self.writer.write_all(COLOR_OFFSET.as_bytes())?;
        }
        if self.show_position_panel {
            match self.squeezer {
                Squeezer::Print => {
                    self.writer.write_all(b"*")?;
                    if self.show_color {
                        self.writer.write_all(COLOR_RESET.as_bytes())?;
                    }
                    self.writer.write_all(b"       ")?;
                }
                Squeezer::Ignore | Squeezer::Disabled | Squeezer::Delete => {
                    let byte_index: [u8; 8] = (self.idx + self.display_offset).to_be_bytes();
                    let mut i = 0;
                    while byte_index[i] == 0x0 && i < 4 {
                        i += 1;
                    }
                    for &byte in byte_index.iter().skip(i) {
                        self.writer
                            .write_all(self.byte_hex_panel_g[byte as usize].as_bytes())?;
                    }
                    if self.show_color {
                        self.writer.write_all(COLOR_RESET.as_bytes())?;
                    }
                }
            }
            self.writer.write_all(
                self.border_style
                    .outer_sep()
                    .encode_utf8(&mut [0; 4])
                    .as_bytes(),
            )?;
        }
        Ok(())
    }

    fn print_char(&mut self, i: u64) -> io::Result<()> {
        match self.squeezer {
            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.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())?;
                } else {
                    self.squeezer = Squeezer::Print;
                }
            }
        }
        if i == 8 * self.panels - 1 {
            if self.show_color {
                self.writer.write_all(COLOR_RESET.as_bytes())?;
                self.curr_color = None;
            }
            self.writer.write_all(
                self.border_style
                    .outer_sep()
                    .encode_utf8(&mut [0; 4])
                    .as_bytes(),
            )?;
        } else if i % 8 == 7 {
            if self.show_color {
                self.writer.write_all(COLOR_RESET.as_bytes())?;
                self.curr_color = None;
            }
            self.writer.write_all(
                self.border_style
                    .inner_sep()
                    .encode_utf8(&mut [0; 4])
                    .as_bytes(),
            )?;
        }

        Ok(())
    }

    pub fn print_char_panel(&mut self) -> io::Result<()> {
        for i in 0..self.line_buf.len() {
            self.print_char(i as u64)?;
        }
        Ok(())
    }

    fn print_byte(&mut self, i: usize, b: u8) -> io::Result<()> {
        match self.squeezer {
            Squeezer::Print => {
                if !self.show_position_panel && i == 0 {
                    if self.show_color {
                        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.as_bytes())?;
                    }
                } else if i.is_multiple_of(self.group_size as usize) {
                    self.writer.write_all(b" ")?;
                }
                for _ in 0..self.base_digits {
                    self.writer.write_all(b" ")?;
                }
            }
            Squeezer::Delete => self.writer.write_all(b"   ")?,
            Squeezer::Ignore | Squeezer::Disabled => {
                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.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())?;
            }
        }
        // byte is last in panel
        if i % 8 == 7 {
            if self.show_color {
                self.curr_color = None;
                self.writer.write_all(COLOR_RESET.as_bytes())?;
            }
            self.writer.write_all(b" ")?;
            // byte is last in last panel
            if i as u64 % (8 * self.panels) == 8 * self.panels - 1 {
                self.writer.write_all(
                    self.border_style
                        .outer_sep()
                        .encode_utf8(&mut [0; 4])
                        .as_bytes(),
                )?;
            } else {
                self.writer.write_all(
                    self.border_style
                        .inner_sep()
                        .encode_utf8(&mut [0; 4])
                        .as_bytes(),
                )?;
            }
        }
        Ok(())
    }

    fn reorder_buffer_to_little_endian(&self, buf: &mut [u8]) {
        let n = buf.len();
        let group_sz = self.group_size as usize;

        for idx in (0..n).step_by(group_sz) {
            let remaining = n - idx;
            let total = remaining.min(group_sz);

            buf[idx..idx + total].reverse();
        }
    }

    pub fn print_bytes(&mut self) -> io::Result<()> {
        let mut buf = self.line_buf.clone();

        if matches!(self.endianness, Endianness::Little) {
            self.reorder_buffer_to_little_endian(&mut buf);
        };

        for (i, &b) in buf.iter().enumerate() {
            self.print_byte(i, b)?;
        }
        Ok(())
    }

    /// Loop through the given `Reader`, printing until the `Reader` buffer
    /// is exhausted.
    pub fn print_all<Reader: Read>(&mut self, reader: Reader) -> io::Result<()> {
        let mut is_empty = true;

        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) {
                if n > 0 && n < 8 * self.panels as usize {
                    // if less are read, that indicates end of file after
                    if is_empty {
                        self.print_header()?;
                        is_empty = false;
                    }
                    let mut leftover = n;
                    // loop until input is ceased
                    if let Some(s) = loop {
                        if let Ok(n) = buf.read(&mut self.line_buf[leftover..]) {
                            leftover += n;
                            // there is no more input being read
                            if n == 0 {
                                self.line_buf.resize(leftover, 0);
                                break Some(leftover);
                            }
                            // amount read has exceeded line buffer
                            if leftover >= 8 * self.panels as usize {
                                break None;
                            }
                        }
                    } {
                        break Some(s);
                    };
                } else if n == 0 {
                    // if no bytes are read, that indicates end of file
                    if self.squeezer == Squeezer::Delete {
                        // empty the last line when ending is squeezed
                        self.line_buf.clear();
                        break Some(0);
                    }
                    break None;
                }
            }
            if is_empty {
                self.print_header()?;
            }

            // squeeze is active, check if the line is the same
            // skip print if still squeezed, otherwise print and deactivate squeeze
            if matches!(self.squeezer, Squeezer::Print | Squeezer::Delete) {
                if self
                    .line_buf
                    .chunks_exact(std::mem::size_of::<usize>())
                    .all(|w| usize::from_ne_bytes(w.try_into().unwrap()) == self.squeeze_byte)
                {
                    if self.squeezer == Squeezer::Delete {
                        self.idx += 8 * self.panels;
                        continue;
                    }
                } else {
                    self.squeezer = Squeezer::Ignore;
                }
            }

            // print the line
            self.print_position_panel()?;
            self.print_bytes()?;
            if self.show_char_panel {
                self.print_char_panel()?;
            }
            self.writer.write_all(b"\n")?;

            if is_empty {
                self.writer.flush()?;
                is_empty = false;
            }

            // increment index to next line
            self.idx += 8 * self.panels;

            // change from print to delete if squeeze is still active
            if self.squeezer == Squeezer::Print {
                self.squeezer = Squeezer::Delete;
            }

            // repeat the first byte in the line until it's a usize
            // compare that usize with each usize chunk in the line
            // if they are all the same, change squeezer to print
            let repeat_byte = (self.line_buf[0] as usize) * (usize::MAX / 255);
            if !matches!(self.squeezer, Squeezer::Disabled | Squeezer::Delete)
                && self
                    .line_buf
                    .chunks_exact(std::mem::size_of::<usize>())
                    .all(|w| usize::from_ne_bytes(w.try_into().unwrap()) == repeat_byte)
            {
                self.squeezer = Squeezer::Print;
                self.squeeze_byte = repeat_byte;
            };
        };

        // special ending

        if is_empty {
            self.base_digits = 2;
            self.print_header()?;
            if self.show_position_panel {
                write!(self.writer, "{0:9}", "│")?;
            }
            write!(
                self.writer,
                "{0:2}{1:2$}{0}{0:>3$}",
                "│",
                "No content",
                self.panel_sz() - 1,
                self.panel_sz() + 1,
            )?;
            if self.show_char_panel {
                write!(self.writer, "{0:>9}{0:>9}", "│")?;
            }
            writeln!(self.writer)?;
        } else if let Some(n) = leftover {
            // last line is incomplete
            self.squeezer = Squeezer::Ignore;
            self.print_position_panel()?;
            self.print_bytes()?;
            self.squeezer = Squeezer::Print;
            for i in n..8 * self.panels as usize {
                self.print_byte(i, 0)?;
            }
            if self.show_char_panel {
                self.squeezer = Squeezer::Ignore;
                self.print_char_panel()?;
                self.squeezer = Squeezer::Print;
                for i in n..8 * self.panels as usize {
                    self.print_char(i as u64)?;
                }
            }
            self.writer.write_all(b"\n")?;
        }

        self.print_footer()?;

        self.writer.flush()?;

        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)]
mod tests {
    use std::io;
    use std::str;

    use super::*;

    fn assert_print_all_output<Reader: Read>(input: Reader, expected_string: String) {
        let mut output = vec![];
        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();

        let actual_string: &str = str::from_utf8(&output).unwrap();
        assert_eq!(actual_string, expected_string,)
    }

    #[test]
    fn empty_file_passes() {
        let input = io::empty();
        let expected_string = "\
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│        │ No content              │                         │        │        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
"
        .to_owned();
        assert_print_all_output(input, expected_string);
    }

    #[test]
    fn short_input_passes() {
        let input = io::Cursor::new(b"spam");
        let expected_string = "\
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 73 70 61 6d             ┊                         │spam    ┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
"
        .to_owned();
        assert_print_all_output(input, expected_string);
    }

    #[test]
    fn display_offset() {
        let input = io::Cursor::new(b"spamspamspamspamspam");
        let expected_string = "\
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│deadbeef│ 73 70 61 6d 73 70 61 6d ┊ 73 70 61 6d 73 70 61 6d │spamspam┊spamspam│
│deadbeff│ 73 70 61 6d             ┊                         │spam    ┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
"
        .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::Off)
            .color_scheme(ColorScheme::Default)
            .build();
        printer.display_offset(0xdeadbeef);

        printer.print_all(input).unwrap();

        let actual_string: &str = str::from_utf8(&output).unwrap();
        assert_eq!(actual_string, expected_string)
    }

    #[test]
    fn multiple_panels() {
        let input = io::Cursor::new(b"supercalifragilisticexpialidocioussupercalifragilisticexpialidocioussupercalifragilisticexpialidocious");
        let expected_string = "\
┌────────┬─────────────────────────┬─────────────────────────┬─────────────────────────┬─────────────────────────┬────────┬────────┬────────┬────────┐
│00000000│ 73 75 70 65 72 63 61 6c ┊ 69 66 72 61 67 69 6c 69 ┊ 73 74 69 63 65 78 70 69 ┊ 61 6c 69 64 6f 63 69 6f │supercal┊ifragili┊sticexpi┊alidocio│
│00000020│ 75 73 73 75 70 65 72 63 ┊ 61 6c 69 66 72 61 67 69 ┊ 6c 69 73 74 69 63 65 78 ┊ 70 69 61 6c 69 64 6f 63 │ussuperc┊alifragi┊listicex┊pialidoc│
│00000040│ 69 6f 75 73 73 75 70 65 ┊ 72 63 61 6c 69 66 72 61 ┊ 67 69 6c 69 73 74 69 63 ┊ 65 78 70 69 61 6c 69 64 │ioussupe┊rcalifra┊gilistic┊expialid│
│00000060│ 6f 63 69 6f 75 73       ┊                         ┊                         ┊                         │ocious  ┊        ┊        ┊        │
└────────┴─────────────────────────┴─────────────────────────┴─────────────────────────┴─────────────────────────┴────────┴────────┴────────┴────────┘
"
        .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(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();

        let actual_string: &str = str::from_utf8(&output).unwrap();
        assert_eq!(actual_string, expected_string)
    }

    #[test]
    fn squeeze_works() {
        let input = io::Cursor::new(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00");
        let expected_string = "\
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│*       │                         ┊                         │        ┊        │
│00000020│ 00                      ┊                         │⋄       ┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
"
        .to_owned();
        assert_print_all_output(input, expected_string);
    }

    #[test]
    fn squeeze_nonzero() {
        let input = io::Cursor::new(b"000000000000000000000000000000000");
        let expected_string = "\
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 30 30 30 30 30 30 30 30 ┊ 30 30 30 30 30 30 30 30 │00000000┊00000000│
│*       │                         ┊                         │        ┊        │
│00000020│ 30                      ┊                         │0       ┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
"
        .to_owned();
        assert_print_all_output(input, expected_string);
    }

    #[test]
    fn squeeze_multiple_panels() {
        let input = io::Cursor::new(b"0000000000000000000000000000000000000000000000000");
        let expected_string = "\
┌────────┬─────────────────────────┬─────────────────────────┬─────────────────────────┬────────┬────────┬────────┐
│00000000│ 30 30 30 30 30 30 30 30 ┊ 30 30 30 30 30 30 30 30 ┊ 30 30 30 30 30 30 30 30 │00000000┊00000000┊00000000│
│*       │                         ┊                         ┊                         │        ┊        ┊        │
│00000030│ 30                      ┊                         ┊                         │0       ┊        ┊        │
└────────┴─────────────────────────┴─────────────────────────┴─────────────────────────┴────────┴────────┴────────┘
"
        .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(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();

        let actual_string: &str = str::from_utf8(&output).unwrap();
        assert_eq!(actual_string, expected_string)
    }
}


================================================
FILE: src/main.rs
================================================
use std::fs::File;
use std::io::{self, prelude::*, BufWriter, SeekFrom};
use std::num::{NonZeroI64, NonZeroU64};
use std::path::PathBuf;

use clap::builder::styling::{AnsiColor, Effects};
use clap::builder::ArgPredicate;
use clap::builder::Styles;
use clap::{ArgAction, CommandFactory, Parser, ValueEnum};
use clap_complete::aot::{generate, Shell};

use anyhow::{anyhow, bail, Context, Result};

use const_format::formatcp;

use thiserror::Error as ThisError;

use terminal_size::terminal_size;

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,
    COLOR_RESET,
};

#[cfg(test)]
mod tests;

const DEFAULT_BLOCK_SIZE: i64 = 512;

const LENGTH_HELP_TEXT: &str = "Only read N bytes from the input. The N argument can also include \
                                a unit with a decimal prefix (kB, MB, ..) or binary prefix (kiB, \
                                MiB, ..), or can be specified using a hex number. The short \
                                option '-l' can be used as an alias.
Examples: --length=64, --length=4KiB, --length=0xff";

const SKIP_HELP_TEXT: &str = "Skip the first N bytes of the input. The N argument can also \
                              include a unit (see `--length` for details).
A negative value is valid and will seek from the end of the file.";

const BLOCK_SIZE_HELP_TEXT: &str = "Sets the size of the `block` unit to SIZE.
Examples: --block-size=1024, --block-size=4kB";

const DISPLAY_OFFSET_HELP_TEXT: &str = "Add N bytes to the displayed file position. The N \
                                        argument can also include a unit (see `--length` for \
                                        details).
A negative value is valid and calculates an offset relative to the end of the file.";

const TERMINAL_WIDTH_HELP_TEXT: &str = "Sets the number of terminal columns to be displayed.
Since the terminal width may not be an evenly divisible by the width per hex data column, this \
                                        will use the greatest number of hex data panels that can \
                                        fit in the requested width but still leave some space to \
                                        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), styles = STYLES)]
struct Opt {
    /// The file to display. If no FILE argument is given, read from STDIN.
    #[arg(value_name("FILE"))]
    file: Option<PathBuf>,

    #[arg(
        help(LENGTH_HELP_TEXT),
        short('n'),
        long,
        visible_short_alias('c'),
        visible_alias("bytes"),
        short_alias('l'),
        value_name("N")
    )]
    length: Option<String>,

    #[arg(help(SKIP_HELP_TEXT), short, long, value_name("N"))]
    skip: Option<String>,

    #[arg(
        help(BLOCK_SIZE_HELP_TEXT),
        long,
        default_value(formatcp!("{DEFAULT_BLOCK_SIZE}")),
        value_name("SIZE")
    )]
    block_size: String,

    /// Displays all input data. Otherwise any number of groups of output lines
    /// which would be identical to the preceding group of lines, are replaced
    /// with a line comprised of a single asterisk.
    #[arg(short('v'), long)]
    no_squeezing: bool,

    /// When to use colors.
    #[arg(
        long,
        value_enum,
        default_value_t,
        value_name("WHEN"),
        default_value_if("plain", ArgPredicate::IsPresent, Some("never"))
    )]
    color: ColorWhen,

    /// Whether to draw a border.
    #[arg(
        long,
        value_enum,
        default_value_t,
        value_name("STYLE"),
        default_value_if("plain", ArgPredicate::IsPresent, Some("none"))
    )]
    border: BorderStyle,

    /// Display output with --no-characters, --no-position, --border=none, and
    /// --color=never.
    #[arg(short, long)]
    plain: bool,

    /// Do not show the character panel on the right.
    #[arg(long)]
    no_characters: bool,

    /// Show the character panel on the right. This is the default, unless
    /// --no-characters has been specified.
    #[arg(
        short('C'),
        long,
        action(ArgAction::SetTrue),
        overrides_with("no_characters")
    )]
    characters: (),

    /// Defines how bytes are mapped to characters.
    #[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,

    #[arg(
        help(DISPLAY_OFFSET_HELP_TEXT),
        short('o'),
        long,
        default_value("0"),
        value_name("N")
    )]
    display_offset: String,

    /// Sets the number of hex data panels to be displayed. `--panels=auto` will
    /// display the maximum number of hex data panels based on the current
    /// terminal width. By default, hexyl will show two panels, unless the
    /// terminal is not wide enough for that.
    #[arg(long, value_name("N"))]
    panels: Option<String>,

    /// Number of bytes/octets that should be grouped together. You can use the
    /// '--endianness' option to control the ordering of the bytes within a
    /// group. '--groupsize' can be used as an alias (xxd-compatibility).
    #[arg(
        short('g'),
        long,
        value_enum,
        default_value_t,
        alias("groupsize"),
        value_name("N")
    )]
    group_size: GroupSize,

    /// Whether to print out groups in little-endian or big-endian format. This
    /// option only has an effect if the '--group-size' is larger than 1. '-e'
    /// can be used as an alias for '--endianness=little'.
    #[arg(long, value_enum, default_value_t, value_name("FORMAT"))]
    endianness: Endianness,

    /// An alias for '--endianness=little'.
    #[arg(short('e'), hide(true), overrides_with("endianness"))]
    little_endian_format: bool,

    /// Sets the base used for the bytes. The possible options are binary,
    /// octal, decimal, and hexadecimal.
    #[arg(short('b'), long, default_value("hexadecimal"), value_name("B"))]
    base: String,

    #[arg(
        help(TERMINAL_WIDTH_HELP_TEXT),
        long,
        value_name("N"),
        conflicts_with("panels")
    )]
    terminal_width: Option<NonZeroU64>,

    /// 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)]
enum ColorWhen {
    /// Always use colorized output.
    #[default]
    Always,

    /// Only displays colors if the output goes to an interactive terminal.
    Auto,

    /// Do not use colorized output.
    Never,

    /// Override the NO_COLOR environment variable.
    Force,
}

#[derive(Clone, Debug, Default, ValueEnum)]
enum GroupSize {
    /// Grouped together every byte/octet.
    #[default]
    #[value(name = "1")]
    One,

    /// Grouped together every 2 bytes/octets.
    #[value(name = "2")]
    Two,

    /// Grouped together every 4 bytes/octets.
    #[value(name = "4")]
    Four,

    /// Grouped together every 8 bytes/octets.
    #[value(name = "8")]
    Eight,
}

impl From<GroupSize> for u8 {
    fn from(number: GroupSize) -> Self {
        match number {
            GroupSize::One => 1,
            GroupSize::Two => 2,
            GroupSize::Four => 4,
            GroupSize::Eight => 8,
        }
    }
}

fn run() -> Result<()> {
    let opt = Opt::parse();

    if opt.print_color_table {
        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 {
        Some(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)
            }
        }
        None => Input::Stdin(stdin.lock()),
    };

    if let Some(hex_number) = try_parse_as_hex_number(&opt.block_size) {
        return hex_number
            .map_err(|e| anyhow!(e))
            .and_then(|x| {
                PositiveI64::new(x).ok_or_else(|| anyhow!("block size argument must be positive"))
            })
            .map(|_| ());
    }
    let (num, unit) = extract_num_and_unit_from(&opt.block_size)?;
    if let Unit::Block { custom_size: _ } = unit {
        return Err(anyhow!(
            "can not use 'block(s)' as a unit to specify block size"
        ));
    };
    let block_size = num
        .checked_mul(unit.get_multiplier())
        .ok_or_else(|| anyhow!(ByteOffsetParseError::UnitMultiplicationOverflow))
        .and_then(|x| {
            PositiveI64::new(x).ok_or_else(|| anyhow!("block size argument must be positive"))
        })?;

    let skip_arg = opt
        .skip
        .as_ref()
        .map(|s| {
            parse_byte_offset(s, block_size).context(anyhow!(
                "failed to parse `--skip` arg {:?} as byte count",
                s
            ))
        })
        .transpose()?;

    let skip_offset = if let Some(ByteOffset { kind, value }) = skip_arg {
        let value = value.into_inner();
        reader
            .seek(match kind {
                ByteOffsetKind::ForwardFromBeginning | ByteOffsetKind::ForwardFromLastOffset => {
                    SeekFrom::Current(value)
                }
                ByteOffsetKind::BackwardFromEnd => SeekFrom::End(value.checked_neg().unwrap()),
            })
            .map_err(|_| {
                anyhow!(
                    "Failed to jump to the desired input position. \
                     This could be caused by a negative offset that is too large or by \
                     an input that is not seek-able (e.g. if the input comes from a pipe)."
                )
            })?
    } else {
        0
    };

    let parse_byte_count = |s| -> Result<u64> {
        Ok(parse_byte_offset(s, block_size)?
            .assume_forward_offset_from_start()?
            .into())
    };

    let mut reader = if let Some(ref length) = opt.length {
        let length = parse_byte_count(length).context(anyhow!(
            "failed to parse `--length` arg {:?} as byte count",
            length
        ))?;
        Box::new(reader.take(length))
    } else {
        reader.into_inner()
    };

    let no_color = std::env::var_os("NO_COLOR").is_some();
    let show_color = match opt.color {
        ColorWhen::Never => false,
        ColorWhen::Always => !no_color,
        ColorWhen::Force => true,
        ColorWhen::Auto => {
            if no_color {
                false
            } else {
                supports_color::on(supports_color::Stream::Stdout)
                    .map(|level| level.has_basic)
                    .unwrap_or(false)
            }
        }
    };

    let border_style = opt.border;

    let &squeeze = &!opt.no_squeezing;

    let show_char_panel = !opt.no_characters && !opt.plain;

    let show_position_panel = !opt.no_position && !opt.plain;

    let display_offset: u64 = parse_byte_count(&opt.display_offset).context(anyhow!(
        "failed to parse `--display-offset` arg {:?} as byte count",
        opt.display_offset
    ))?;

    let max_panels_fn = |terminal_width: u64, base_digits: u64, group_size: u64| {
        let offset = if show_position_panel { 10 } else { 1 };
        let col_width = if show_char_panel {
            ((8 / group_size) * (base_digits * group_size + 1)) + 2 + 8
        } else {
            ((8 / group_size) * (base_digits * group_size + 1)) + 2
        };
        if (terminal_width.saturating_sub(offset)) / col_width < 1 {
            1
        } else {
            (terminal_width - offset) / col_width
        }
    };

    let base = if let Ok(base_num) = opt.base.parse::<u8>() {
        match base_num {
            2 => Ok(Base::Binary),
            8 => Ok(Base::Octal),
            10 => Ok(Base::Decimal),
            16 => Ok(Base::Hexadecimal),
            _ => Err(anyhow!(
                "The number provided is not a valid base. Valid bases are 2, 8, 10, and 16."
            )),
        }
    } else {
        match opt.base.as_str() {
            "b" | "bin" | "binary" => Ok(Base::Binary),
            "o" | "oct" | "octal" => Ok(Base::Octal),
            "d" | "dec" | "decimal" => Ok(Base::Decimal),
            "x" | "hex" | "hexadecimal" => Ok(Base::Hexadecimal),
            _ => Err(anyhow!(
                "The base provided is not valid. Valid bases are \"b\", \"o\", \"d\", and \"x\"."
            )),
        }
    }?;

    let base_digits = match base {
        Base::Binary => 8,
        Base::Octal => 3,
        Base::Decimal => 3,
        Base::Hexadecimal => 2,
    };

    let group_size = u8::from(opt.group_size);

    let terminal_width = terminal_size().map(|s| s.0 .0 as u64).unwrap_or(80);

    let panels = if opt.panels.as_deref() == Some("auto") {
        max_panels_fn(terminal_width, base_digits, group_size.into())
    } else if let Some(panels) = opt.panels {
        panels
            .parse::<NonZeroU64>()
            .map(u64::from)
            .context(anyhow!(
                "failed to parse `--panels` arg {:?} as unsigned nonzero integer",
                panels
            ))?
    } else if let Some(terminal_width) = opt.terminal_width {
        max_panels_fn(terminal_width.into(), base_digits, group_size.into())
    } else {
        std::cmp::min(
            2,
            max_panels_fn(terminal_width, base_digits, group_size.into()),
        )
    };

    let endianness = if opt.little_endian_format {
        Endianness::Little
    } else {
        opt.endianness
    };

    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)
        .show_position_panel(show_position_panel)
        .with_border_style(border_style)
        .enable_squeezing(squeeze)
        .num_panels(panels)
        .group_size(group_size)
        .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))?;

    Ok(())
}

fn main() {
    let result = run();

    if let Err(err) = result {
        if let Some(io_error) = err.downcast_ref::<io::Error>() {
            if io_error.kind() == ::std::io::ErrorKind::BrokenPipe {
                std::process::exit(0);
            }
        }
        eprintln!("Error: {err:?}");
        std::process::exit(1);
    }
}

#[derive(Clone, Copy, Debug, Default, Hash, Eq, Ord, PartialEq, PartialOrd)]
pub struct NonNegativeI64(i64);

impl NonNegativeI64 {
    pub fn new(x: i64) -> Option<Self> {
        if x.is_negative() {
            None
        } else {
            Some(Self(x))
        }
    }

    pub fn into_inner(self) -> i64 {
        self.0
    }
}

impl From<NonNegativeI64> for u64 {
    fn from(x: NonNegativeI64) -> u64 {
        u64::try_from(x.0)
            .expect("invariant broken: NonNegativeI64 should contain a non-negative i64 value")
    }
}

fn print_color_table() -> io::Result<()> {
    let mut stdout = BufWriter::new(io::stdout().lock());

    writeln!(stdout, "hexyl color reference:\n")?;

    // NULL bytes
    stdout.write_all(COLOR_NULL.as_bytes())?;
    writeln!(stdout, "⋄ NULL bytes (0x00)")?;
    stdout.write_all(COLOR_RESET.as_bytes())?;

    // ASCII printable
    stdout.write_all(COLOR_ASCII_PRINTABLE.as_bytes())?;
    writeln!(stdout, "a ASCII printable characters (0x20 - 0x7E)")?;
    stdout.write_all(COLOR_RESET.as_bytes())?;

    // ASCII whitespace
    stdout.write_all(COLOR_ASCII_WHITESPACE.as_bytes())?;
    writeln!(stdout, "_ ASCII whitespace (0x09 - 0x0D, 0x20)")?;
    stdout.write_all(COLOR_RESET.as_bytes())?;

    // ASCII other
    stdout.write_all(COLOR_ASCII_OTHER.as_bytes())?;
    writeln!(
        stdout,
        "• ASCII control characters (except NULL and whitespace)"
    )?;
    stdout.write_all(COLOR_RESET.as_bytes())?;

    // Non-ASCII
    stdout.write_all(COLOR_NONASCII.as_bytes())?;
    writeln!(stdout, "× Non-ASCII bytes (0x80 - 0xFF)")?;
    stdout.write_all(COLOR_RESET.as_bytes())?;

    Ok(())
}

#[derive(Clone, Copy, Debug, Default, Hash, Eq, Ord, PartialEq, PartialOrd)]
pub struct PositiveI64(i64);

impl PositiveI64 {
    pub fn new(x: i64) -> Option<Self> {
        if x < 1 {
            None
        } else {
            Some(Self(x))
        }
    }

    pub fn into_inner(self) -> i64 {
        self.0
    }
}

impl From<PositiveI64> for u64 {
    fn from(x: PositiveI64) -> u64 {
        u64::try_from(x.0)
            .expect("invariant broken: PositiveI64 should contain a positive i64 value")
    }
}

#[derive(Debug, PartialEq)]
enum Unit {
    Byte,
    Kilobyte,
    Megabyte,
    Gigabyte,
    Terabyte,
    Kibibyte,
    Mebibyte,
    Gibibyte,
    Tebibyte,
    /// a customizable amount of bytes
    Block {
        custom_size: Option<NonZeroI64>,
    },
}

impl Unit {
    const fn get_multiplier(self) -> i64 {
        match self {
            Self::Byte => 1,
            Self::Kilobyte => 1000,
            Self::Megabyte => 1_000_000,
            Self::Gigabyte => 1_000_000_000,
            Self::Terabyte => 1_000_000_000_000,
            Self::Kibibyte => 1 << 10,
            Self::Mebibyte => 1 << 20,
            Self::Gibibyte => 1 << 30,
            Self::Tebibyte => 1 << 40,
            Self::Block {
                custom_size: Some(size),
            } => size.get(),
            Self::Block { custom_size: None } => DEFAULT_BLOCK_SIZE,
        }
    }
}

const HEX_PREFIX: &str = "0x";

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
enum ByteOffsetKind {
    ForwardFromBeginning,
    ForwardFromLastOffset,
    BackwardFromEnd,
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct ByteOffset {
    value: NonNegativeI64,
    kind: ByteOffsetKind,
}

#[derive(Clone, Debug, ThisError)]
#[error(
    "negative offset specified, but only positive offsets (counts) are accepted in this context"
)]
struct NegativeOffsetSpecifiedError;

impl ByteOffset {
    fn assume_forward_offset_from_start(
        &self,
    ) -> Result<NonNegativeI64, NegativeOffsetSpecifiedError> {
        let &Self { value, kind } = self;
        match kind {
            ByteOffsetKind::ForwardFromBeginning | ByteOffsetKind::ForwardFromLastOffset => {
                Ok(value)
            }
            ByteOffsetKind::BackwardFromEnd => Err(NegativeOffsetSpecifiedError),
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
enum ByteOffsetParseError {
    #[error("no character data found, did you forget to write it?")]
    Empty,
    #[error("no digits found after sign, did you forget to write them?")]
    EmptyAfterSign,
    #[error(
        "found {0:?} sign after hex prefix ({:?}); signs should go before it",
        HEX_PREFIX
    )]
    SignFoundAfterHexPrefix(char),
    #[error("{0:?} is not of the expected form <pos-integer>[<unit>]")]
    InvalidNumAndUnit(String),
    #[error("{0:?} is a valid unit, but an integer should come before it")]
    EmptyWithUnit(String),
    #[error("invalid unit {0:?}")]
    InvalidUnit(String),
    #[error("failed to parse integer part")]
    ParseNum(#[source] std::num::ParseIntError),
    #[error("count multiplied by the unit overflowed a signed 64-bit integer; are you sure it should be that big?")]
    UnitMultiplicationOverflow,
}

fn parse_byte_offset(n: &str, block_size: PositiveI64) -> Result<ByteOffset, ByteOffsetParseError> {
    use ByteOffsetParseError::*;

    let (n, kind) = process_sign_of(n)?;

    let into_byte_offset = |value| {
        Ok(ByteOffset {
            value: NonNegativeI64::new(value).unwrap(),
            kind,
        })
    };

    if let Some(hex_number) = try_parse_as_hex_number(n) {
        return hex_number.map(into_byte_offset)?;
    }

    let (num, mut unit) = extract_num_and_unit_from(n)?;
    if let Unit::Block { custom_size: None } = unit {
        unit = Unit::Block {
            custom_size: Some(
                NonZeroI64::new(block_size.into_inner()).expect("PositiveI64 was zero"),
            ),
        };
    }

    num.checked_mul(unit.get_multiplier())
        .ok_or(UnitMultiplicationOverflow)
        .and_then(into_byte_offset)
}

/// Takes a string containing a base-10 number and an optional unit, and returns them with their proper types.
/// The unit must directly follow the number (e.g. no whitespace is allowed between them).
/// When no unit is given, [Unit::Byte] is assumed.
/// When the unit is [Unit::Block], it is returned without custom size.
/// No normalization is performed, that is "1024" is extracted to (1024, Byte), not (1, Kibibyte).
fn extract_num_and_unit_from(n: &str) -> Result<(i64, Unit), ByteOffsetParseError> {
    use ByteOffsetParseError::*;
    if n.is_empty() {
        return Err(Empty);
    }
    match n.chars().position(|c| !c.is_ascii_digit()) {
        Some(unit_begin_idx) => {
            let (n, raw_unit) = n.split_at(unit_begin_idx);
            let unit = match raw_unit.to_lowercase().as_str() {
                "" => Unit::Byte, // no "b" => Byte to allow hex nums with units
                "kb" => Unit::Kilobyte,
                "mb" => Unit::Megabyte,
                "gb" => Unit::Gigabyte,
                "tb" => Unit::Terabyte,
                "kib" => Unit::Kibibyte,
                "mib" => Unit::Mebibyte,
                "gib" => Unit::Gibibyte,
                "tib" => Unit::Tebibyte,
                "block" | "blocks" => Unit::Block { custom_size: None },
                _ => {
                    return if n.is_empty() {
                        Err(InvalidNumAndUnit(raw_unit.to_string()))
                    } else {
                        Err(InvalidUnit(raw_unit.to_string()))
                    }
                }
            };
            let num = n.parse::<i64>().map_err(|e| {
                if n.is_empty() {
                    EmptyWithUnit(raw_unit.to_owned())
                } else {
                    ParseNum(e)
                }
            })?;
            Ok((num, unit))
        }
        None => {
            // no unit part
            let num = n.parse::<i64>().map_err(ParseNum)?;
            Ok((num, Unit::Byte))
        }
    }
}

/// Extracts a [ByteOffsetKind] based on the sign at the beginning of the given string.
/// Returns the input string without the sign (or an equal string if there wasn't any sign).
fn process_sign_of(n: &str) -> Result<(&str, ByteOffsetKind), ByteOffsetParseError> {
    use ByteOffsetParseError::*;
    let mut chars = n.chars();
    let next_char = chars.next();
    let check_empty_after_sign = || {
        if chars.clone().next().is_none() {
            Err(EmptyAfterSign)
        } else {
            Ok(chars.as_str())
        }
    };
    match next_char {
        Some('+') => Ok((
            check_empty_after_sign()?,
            ByteOffsetKind::ForwardFromLastOffset,
        )),
        Some('-') => Ok((check_empty_after_sign()?, ByteOffsetKind::BackwardFromEnd)),
        None => Err(Empty),
        _ => Ok((n, ByteOffsetKind::ForwardFromBeginning)),
    }
}

/// If `n` starts with a hex prefix, its remaining part is returned as some number (if possible),
/// otherwise None is returned.
fn try_parse_as_hex_number(n: &str) -> Option<Result<i64, ByteOffsetParseError>> {
    use ByteOffsetParseError::*;
    n.strip_prefix(HEX_PREFIX).map(|num| {
        let mut chars = num.chars();
        match chars.next() {
            Some(c @ '+') | Some(c @ '-') => {
                return if chars.next().is_none() {
                    Err(EmptyAfterSign)
                } else {
                    Err(SignFoundAfterHexPrefix(c))
                }
            }
            _ => (),
        }
        i64::from_str_radix(num, 16).map_err(ParseNum)
    })
}


================================================
FILE: src/tests.rs
================================================
use super::*;

#[test]
fn unit_multipliers() {
    use Unit::*;
    assert_eq!(Kilobyte.get_multiplier(), 1000 * Byte.get_multiplier());
    assert_eq!(Megabyte.get_multiplier(), 1000 * Kilobyte.get_multiplier());
    assert_eq!(Gigabyte.get_multiplier(), 1000 * Megabyte.get_multiplier());
    assert_eq!(Terabyte.get_multiplier(), 1000 * Gigabyte.get_multiplier());

    assert_eq!(Kibibyte.get_multiplier(), 1024 * Byte.get_multiplier());
    assert_eq!(Mebibyte.get_multiplier(), 1024 * Kibibyte.get_multiplier());
    assert_eq!(Gibibyte.get_multiplier(), 1024 * Mebibyte.get_multiplier());
    assert_eq!(Tebibyte.get_multiplier(), 1024 * Gibibyte.get_multiplier());
}

#[test]
fn test_process_sign() {
    use ByteOffsetKind::*;
    use ByteOffsetParseError::*;
    assert_eq!(process_sign_of("123"), Ok(("123", ForwardFromBeginning)));
    assert_eq!(process_sign_of("+123"), Ok(("123", ForwardFromLastOffset)));
    assert_eq!(process_sign_of("-123"), Ok(("123", BackwardFromEnd)));
    assert_eq!(process_sign_of("-"), Err(EmptyAfterSign));
    assert_eq!(process_sign_of("+"), Err(EmptyAfterSign));
    assert_eq!(process_sign_of(""), Err(Empty));
}

#[test]
fn test_parse_as_hex() {
    assert_eq!(try_parse_as_hex_number("73"), None);
    assert_eq!(try_parse_as_hex_number("0x1337"), Some(Ok(0x1337)));
    assert!(matches!(try_parse_as_hex_number("0xnope"), Some(Err(_))));
    assert!(matches!(try_parse_as_hex_number("0x-1"), Some(Err(_))));
}

#[test]
fn extract_num_and_unit() {
    use ByteOffsetParseError::*;
    use Unit::*;
    // byte is default unit
    assert_eq!(extract_num_and_unit_from("4"), Ok((4, Byte)));
    // blocks are returned without customization
    assert_eq!(
        extract_num_and_unit_from("2blocks"),
        Ok((2, Block { custom_size: None }))
    );
    // no normalization is performed
    assert_eq!(extract_num_and_unit_from("1024kb"), Ok((1024, Kilobyte)));

    // unit without number results in error
    assert_eq!(
        extract_num_and_unit_from("gib"),
        Err(EmptyWithUnit("gib".to_string()))
    );
    // empty string results in error
    assert_eq!(extract_num_and_unit_from(""), Err(Empty));
    // an invalid unit results in an error
    assert_eq!(
        extract_num_and_unit_from("25litres"),
        Err(InvalidUnit("litres".to_string()))
    );
}

#[test]
fn test_parse_byte_offset() {
    use ByteOffsetParseError::*;

    macro_rules! success {
        ($input: expr, $expected_kind: ident $expected_value: expr) => {
            success!($input, $expected_kind $expected_value; block_size: DEFAULT_BLOCK_SIZE)
        };
        ($input: expr, $expected_kind: ident $expected_value: expr; block_size: $block_size: expr) => {
            assert_eq!(
                parse_byte_offset($input, PositiveI64::new($block_size).unwrap()),
                Ok(
                    ByteOffset {
                        value: NonNegativeI64::new($expected_value).unwrap(),
                        kind: ByteOffsetKind::$expected_kind,
                    }
                ),
            );
        };
    }

    macro_rules! error {
        ($input: expr, $expected_err: expr) => {
            assert_eq!(
                parse_byte_offset($input, PositiveI64::new(DEFAULT_BLOCK_SIZE).unwrap()),
                Err($expected_err),
            );
        };
    }

    success!("0", ForwardFromBeginning 0);
    success!("1", ForwardFromBeginning 1);
    success!("1", ForwardFromBeginning 1);
    success!("100", ForwardFromBeginning 100);
    success!("+100", ForwardFromLastOffset 100);

    success!("0x0", ForwardFromBeginning 0);
    success!("0xf", ForwardFromBeginning 15);
    success!("0xdeadbeef", ForwardFromBeginning 3_735_928_559);

    success!("1KB", ForwardFromBeginning 1000);
    success!("2MB", ForwardFromBeginning 2000000);
    success!("3GB", ForwardFromBeginning 3000000000);
    success!("4TB", ForwardFromBeginning 4000000000000);
    success!("+4TB", ForwardFromLastOffset 4000000000000);

    success!("1GiB", ForwardFromBeginning 1073741824);
    success!("2TiB", ForwardFromBeginning 2199023255552);
    success!("+2TiB", ForwardFromLastOffset 2199023255552);

    success!("0xff", ForwardFromBeginning 255);
    success!("0xEE", ForwardFromBeginning 238);
    success!("+0xFF", ForwardFromLastOffset 255);

    success!("1block", ForwardFromBeginning 512; block_size: 512);
    success!("2block", ForwardFromBeginning 1024; block_size: 512);
    success!("1block", ForwardFromBeginning 4; block_size: 4);
    success!("2block", ForwardFromBeginning 8; block_size: 4);

    // empty string is invalid
    error!("", Empty);
    // These are also bad.
    error!("+", EmptyAfterSign);
    error!("-", EmptyAfterSign);
    error!("K", InvalidNumAndUnit("K".to_owned()));
    error!("k", InvalidNumAndUnit("k".to_owned()));
    error!("m", InvalidNumAndUnit("m".to_owned()));
    error!("block", EmptyWithUnit("block".to_owned()));
    // leading/trailing space is invalid
    error!(" 0", InvalidNumAndUnit(" 0".to_owned()));
    error!("0 ", InvalidUnit(" ".to_owned()));
    // Signs after the hex prefix make no sense
    error!("0x-12", SignFoundAfterHexPrefix('-'));
    // This was previously accepted but shouldn't be.
    error!("0x+12", SignFoundAfterHexPrefix('+'));
    // invalid suffix
    error!("1234asdf", InvalidUnit("asdf".to_owned()));
    // bad numbers
    error!("asdf1234", InvalidNumAndUnit("asdf1234".to_owned()));
    error!("a1s2d3f4", InvalidNumAndUnit("a1s2d3f4".to_owned()));
    // multiplication overflows u64
    error!("20000000TiB", UnitMultiplicationOverflow);

    assert!(
        match parse_byte_offset("99999999999999999999", PositiveI64::new(512).unwrap()) {
            // We can't check against the kind of the `ParseIntError`, so we'll just make sure it's the
            // same as trying to do the parse directly.
            Err(ParseNum(e)) => e == "99999999999999999999".parse::<i64>().unwrap_err(),
            _ => false,
        }
    );
}


================================================
FILE: tests/examples/.gitattributes
================================================
ascii text eol=lf


================================================
FILE: tests/examples/ascii
================================================
0123456789abcde


================================================
FILE: tests/examples/empty
================================================


================================================
FILE: tests/integration_tests.rs
================================================
use assert_cmd::Command;

fn hexyl() -> Command {
    let mut cmd = Command::new(assert_cmd::cargo_bin!("hexyl"));
    cmd.current_dir("tests/examples");
    cmd
}
trait PrettyAssert<S>
where
    S: AsRef<str>,
{
    fn pretty_stdout(self, other: S);
}

// https://github.com/assert-rs/assert_cmd/issues/121#issuecomment-849937376
//
impl<S> PrettyAssert<S> for assert_cmd::assert::Assert
where
    S: AsRef<str>,
{
    fn pretty_stdout(self, other: S) {
        println!("{}", other.as_ref().len());
        let self_str = String::from_utf8(self.get_output().stdout.clone()).unwrap();
        println!("{}", self_str.len());
        pretty_assertions::assert_eq!(self_str, other.as_ref());
    }
}

mod basic {
    use super::hexyl;

    #[test]
    fn can_print_simple_ascii_file() {
        hexyl()
        .arg("ascii")
        .arg("--color=never")
        .assert()
        .success()
        .stdout(
            "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
             │00000000│ 30 31 32 33 34 35 36 37 ┊ 38 39 61 62 63 64 65 0a │01234567┊89abcde_│\n\
             └────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
        );
    }

    #[test]
    fn can_read_input_from_stdin() {
        hexyl()
        .arg("--color=never")
        .write_stdin("abc")
        .assert()
        .success()
        .stdout(
            "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
             │00000000│ 61 62 63                ┊                         │abc     ┊        │\n\
             └────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
        );
    }

    #[test]
    fn fails_on_non_existing_input() {
        hexyl().arg("non-existing").assert().failure();
    }

    #[test]
    fn prints_warning_on_empty_content() {
        hexyl()
        .arg("empty")
        .arg("--color=never")
        .assert()
        .success()
        .stdout(
            "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
             │        │ No content              │                         │        │        │\n\
             └────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
        );
    }
}

mod length {
    use super::hexyl;

    #[test]
    fn length_restricts_output_size() {
        hexyl()
        .arg("hello_world_elf64")
        .arg("--color=never")
        .arg("--length=32")
        .assert()
        .success()
        .stdout(
            "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
             │00000000│ 7f 45 4c 46 02 01 01 00 ┊ 00 00 00 00 00 00 00 00 │•ELF•••⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│\n\
             │00000010│ 02 00 3e 00 01 00 00 00 ┊ 00 10 40 00 00 00 00 00 │•⋄>⋄•⋄⋄⋄┊⋄•@⋄⋄⋄⋄⋄│\n\
             └────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
        );
    }

    #[test]
    fn fail_if_length_and_bytes_options_are_used_simultaneously() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--length=32")
            .arg("--bytes=10")
            .assert()
            .failure();
    }

    #[test]
    fn fail_if_length_and_count_options_are_used_simultaneously() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--length=32")
            .arg("-l=10")
            .assert()
            .failure();
    }
}

mod bytes {
    use super::hexyl;

    #[test]
    fn fail_if_bytes_and_count_options_are_used_simultaneously() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--bytes=32")
            .arg("-l=10")
            .assert()
            .failure();
    }
}

mod skip {
    use super::hexyl;

    #[test]
    fn basic() {
        hexyl()
        .arg("ascii")
        .arg("--color=never")
        .arg("--skip=2")
        .arg("--length=4")
        .assert()
        .success()
        .stdout(
            "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
             │00000002│ 32 33 34 35             ┊                         │2345    ┊        │\n\
             └────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
        );
    }

    #[test]
    fn prints_warning_when_skipping_past_the_end() {
        hexyl()
        .arg("ascii")
        .arg("--color=never")
        .arg("--skip=1000")
        .assert()
        .success()
        .stdout(
            "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
             │        │ No content              │                         │        │        │\n\
             └────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
        );
    }

    #[test]
    fn negative_offset() {
        hexyl()
        .arg("ascii")
        .arg("--color=never")
        .arg("--skip=-4")
        .arg("--length=3")
        .assert()
        .success()
        .stdout(
            "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
             │0000000c│ 63 64 65                ┊                         │cde     ┊        │\n\
             └────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
        );
    }

    #[test]
    fn fails_if_negative_offset_is_too_large() {
        hexyl()
            .arg("ascii")
            .arg("--color=never")
            .arg("--skip=-1MiB")
            .assert()
            .failure()
            .stderr(predicates::str::contains("Failed to jump"));
    }
}

mod display_offset {
    use super::hexyl;

    #[test]
    fn basic() {
        hexyl()
        .arg("ascii")
        .arg("--color=never")
        .arg("--display-offset=0xc0ffee")
        .assert()
        .success()
        .stdout(
            "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
             │00c0ffee│ 30 31 32 33 34 35 36 37 ┊ 38 39 61 62 63 64 65 0a │01234567┊89abcde_│\n\
             └────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
        );
    }

    #[test]
    fn display_offset_and_skip() {
        hexyl()
        .arg("hello_world_elf64")
        .arg("--color=never")
        .arg("--display-offset=0x20")
        .arg("--skip=0x10")
        .arg("--length=0x10")
        .assert()
        .success()
        .stdout(
            "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
             │00000030│ 02 00 3e 00 01 00 00 00 ┊ 00 10 40 00 00 00 00 00 │•⋄>⋄•⋄⋄⋄┊⋄•@⋄⋄⋄⋄⋄│\n\
             └────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
        );
    }
}

mod blocksize {
    use super::hexyl;

    #[test]
    fn fails_for_zero_or_negative_blocksize() {
        hexyl()
            .arg("ascii")
            .arg("--block-size=0")
            .assert()
            .failure();

        hexyl()
            .arg("ascii")
            .arg("--block-size=-16")
            .assert()
            .failure();
    }
}

mod display_settings {
    use super::hexyl;

    #[test]
    fn plain() {
        hexyl()
            .arg("ascii")
            .arg("--plain")
            .assert()
            .success()
            .stdout("  30 31 32 33 34 35 36 37   38 39 61 62 63 64 65 0a  \n");
    }

    #[test]
    fn no_chars() {
        hexyl()
            .arg("ascii")
            .arg("--no-characters")
            .arg("--color=never")
            .assert()
            .success()
            .stdout(
                "┌────────┬─────────────────────────┬─────────────────────────┐\n\
                 │00000000│ 30 31 32 33 34 35 36 37 ┊ 38 39 61 62 63 64 65 0a │\n\
                 └────────┴─────────────────────────┴─────────────────────────┘\n",
            );
    }

    #[test]
    fn no_position() {
        hexyl()
            .arg("ascii")
            .arg("--no-position")
            .arg("--color=never")
            .assert()
            .success()
            .stdout(
                "┌─────────────────────────┬─────────────────────────┬────────┬────────┐\n\
                 │ 30 31 32 33 34 35 36 37 ┊ 38 39 61 62 63 64 65 0a │01234567┊89abcde_│\n\
                 └─────────────────────────┴─────────────────────────┴────────┴────────┘\n",
            );
    }
}

mod group_and_endianness {
    use super::hexyl;
    use super::PrettyAssert;

    #[test]
    fn group_2_bytes_be() {
        hexyl()
            .arg("ascii")
            .arg("--color=never")
            .arg("--group-size=2")
            .assert()
            .success()
            .stdout(
                "┌────────┬─────────────────────┬─────────────────────┬────────┬────────┐\n\
                 │00000000│ 3031 3233 3435 3637 ┊ 3839 6162 6364 650a │01234567┊89abcde_│\n\
                 └────────┴─────────────────────┴─────────────────────┴────────┴────────┘\n",
            );
    }

    #[test]
    fn group_2_bytes_le() {
        hexyl()
            .arg("ascii")
            .arg("--color=never")
            .arg("--group-size=2")
            .arg("--endianness=little")
            .assert()
            .success()
            .stdout(
                "┌────────┬─────────────────────┬─────────────────────┬────────┬────────┐\n\
                 │00000000│ 3130 3332 3534 3736 ┊ 3938 6261 6463 0a65 │01234567┊89abcde_│\n\
                 └────────┴─────────────────────┴─────────────────────┴────────┴────────┘\n",
            );
    }

    #[test]
    fn group_4_bytes_be() {
        hexyl()
            .arg("ascii")
            .arg("--color=never")
            .arg("--group-size=4")
            .assert()
            .success()
            .stdout(
                "┌────────┬───────────────────┬───────────────────┬────────┬────────┐\n\
                 │00000000│ 30313233 34353637 ┊ 38396162 6364650a │01234567┊89abcde_│\n\
                 └────────┴───────────────────┴───────────────────┴────────┴────────┘\n",
            );
    }

    #[test]
    fn group_4_bytes_le() {
        hexyl()
            .arg("ascii")
            .arg("--color=never")
            .arg("--group-size=4")
            .arg("--endianness=little")
            .assert()
            .success()
            .stdout(
                "┌────────┬───────────────────┬───────────────────┬────────┬────────┐\n\
                 │00000000│ 33323130 37363534 ┊ 62613938 0a656463 │01234567┊89abcde_│\n\
                 └────────┴───────────────────┴───────────────────┴────────┴────────┘\n",
            );
    }

    #[test]
    fn group_8_bytes_be() {
        hexyl()
            .arg("ascii")
            .arg("--color=never")
            .arg("--group-size=8")
            .assert()
            .success()
            .stdout(
                "┌────────┬──────────────────┬──────────────────┬────────┬────────┐\n\
                 │00000000│ 3031323334353637 ┊ 383961626364650a │01234567┊89abcde_│\n\
                 └────────┴──────────────────┴──────────────────┴────────┴────────┘\n",
            );
    }

    #[test]
    fn group_8_bytes_le() {
        hexyl()
            .arg("ascii")
            .arg("--color=never")
            .arg("--group-size=8")
            .arg("--endianness=little")
            .assert()
            .success()
            .stdout(
                "┌────────┬──────────────────┬──────────────────┬────────┬────────┐\n\
                 │00000000│ 3736353433323130 ┊ 0a65646362613938 │01234567┊89abcde_│\n\
                 └────────┴──────────────────┴──────────────────┴────────┴────────┘\n",
            );
    }

    #[test]
    fn group_size_plain() {
        hexyl()
            .arg("ascii")
            .arg("--color=never")
            .arg("--plain")
            .arg("--group-size=2")
            .assert()
            .success()
            .stdout("  3031 3233 3435 3637   3839 6162 6364 650a  \n");
    }

    #[test]
    fn group_size_fill_space() {
        hexyl()
            .arg("--color=never")
            .arg("--group-size=2")
            .write_stdin("abc")
            .assert()
            .success()
            .stdout(
                "┌────────┬─────────────────────┬─────────────────────┬────────┬────────┐\n\
                 │00000000│ 6162 63             ┊                     │abc     ┊        │\n\
                 └────────┴─────────────────────┴─────────────────────┴────────┴────────┘\n",
            );
    }

    #[test]
    fn group_size_invalid() {
        hexyl()
            .arg("ascii")
            .arg("--color=never")
            .arg("--plain")
            .arg("--group-size=3")
            .assert()
            .failure();
    }
    #[test]
    fn squeeze_no_chars() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--color=never")
            .arg("--skip=1024")
            .arg("--length=4096")
            .arg("--no-characters")
            .assert()
            .success()
            .pretty_stdout(
                "\
┌────────┬─────────────────────────┬─────────────────────────┐
│00000400│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │
│*       │                         ┊                         │
│00001000│ ba 0e 00 00 00 b9 00 20 ┊ 40 00 bb 01 00 00 00 b8 │
│00001010│ 04 00 00 00 cd 80 b8 01 ┊ 00 00 00 cd 80 00 00 00 │
│00001020│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │
│*       │                         ┊                         │
│00001400│                         ┊                         │
└────────┴─────────────────────────┴─────────────────────────┘
",
            );
    }
    #[test]
    fn squeeze_no_chars_one_panel() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--color=never")
            .arg("--skip=1024")
            .arg("--length=4096")
            .arg("--no-characters")
            .arg("--panels=1")
            .assert()
            .success()
            .pretty_stdout(
                "\
┌────────┬─────────────────────────┐
│00000400│ 00 00 00 00 00 00 00 00 │
│*       │                         │
│00001000│ ba 0e 00 00 00 b9 00 20 │
│00001008│ 40 00 bb 01 00 00 00 b8 │
│00001010│ 04 00 00 00 cd 80 b8 01 │
│00001018│ 00 00 00 cd 80 00 00 00 │
│00001020│ 00 00 00 00 00 00 00 00 │
│*       │                         │
│00001400│                         │
└────────┴─────────────────────────┘
",
            );
    }
    #[test]
    fn squeeze_no_position() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--color=never")
            .arg("--skip=1024")
            .arg("--length=4096")
            .arg("--no-position")
            .assert()
            .success()
            .pretty_stdout(
                "\
┌─────────────────────────┬─────────────────────────┬────────┬────────┐
│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│*                        ┊                         │        ┊        │
│ ba 0e 00 00 00 b9 00 20 ┊ 40 00 bb 01 00 00 00 b8 │ו⋄⋄⋄×⋄ ┊@⋄ו⋄⋄⋄×│
│ 04 00 00 00 cd 80 b8 01 ┊ 00 00 00 cd 80 00 00 00 │•⋄⋄⋄××ו┊⋄⋄⋄××⋄⋄⋄│
│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│*                        ┊                         │        ┊        │
│*                        ┊                         │        ┊        │
└─────────────────────────┴─────────────────────────┴────────┴────────┘
",
            );
    }
    #[test]
    fn squeeze_no_position_one_panel() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--color=never")
            .arg("--skip=1024")
            .arg("--length=4096")
            .arg("--no-position")
            .arg("--panels=1")
            .assert()
            .success()
            .pretty_stdout(
                "\
┌─────────────────────────┬────────┐
│ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄│
│*                        │        │
│ ba 0e 00 00 00 b9 00 20 │ו⋄⋄⋄×⋄ │
│ 40 00 bb 01 00 00 00 b8 │@⋄ו⋄⋄⋄×│
│ 04 00 00 00 cd 80 b8 01 │•⋄⋄⋄××ו│
│ 00 00 00 cd 80 00 00 00 │⋄⋄⋄××⋄⋄⋄│
│ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄│
│*                        │        │
│*                        │        │
└─────────────────────────┴────────┘
",
            );
    }
    #[test]
    fn squeeze_odd_panels_remainder_bytes() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--color=never")
            .arg("--skip=1024")
            .arg("--length=4092") // 4 byte remainder
            .arg("--panels=3")
            .assert()
            .success()
            .pretty_stdout(
                "\
┌────────┬─────────────────────────┬─────────────────────────┬─────────────────────────┬────────┬────────┬────────┐
│00000400│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│*       │                         ┊                         ┊                         │        ┊        ┊        │
│00001000│ ba 0e 00 00 00 b9 00 20 ┊ 40 00 bb 01 00 00 00 b8 ┊ 04 00 00 00 cd 80 b8 01 │ו⋄⋄⋄×⋄ ┊@⋄ו⋄⋄⋄×┊•⋄⋄⋄××ו│
│00001018│ 00 00 00 cd 80 00 00 00 ┊ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄××⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│00001030│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│*       │                         ┊                         ┊                         │        ┊        ┊        │
│000013f0│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00             ┊                         │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄    ┊        │
└────────┴─────────────────────────┴─────────────────────────┴─────────────────────────┴────────┴────────┴────────┘
",
            );
    }

    #[test]
    fn squeeze_plain() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--color=never")
            .arg("--skip=1024")
            .arg("--length=4096")
            .arg("--plain")
            .assert()
            .success()
            .pretty_stdout(
                "  \
  00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00  
 *                                                   
  ba 0e 00 00 00 b9 00 20   40 00 bb 01 00 00 00 b8  
  04 00 00 00 cd 80 b8 01   00 00 00 cd 80 00 00 00  
  00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00  
 *                                                   
 *                                                   
",
            );
    }

    #[test]
    fn squeeze_plain_remainder() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--color=never")
            .arg("--skip=1024")
            .arg("--length=4092") // 4 byte remainder
            .arg("--plain")
            .assert()
            .success()
            .pretty_stdout(
                "  \
  00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00  
 *                                                   
  ba 0e 00 00 00 b9 00 20   40 00 bb 01 00 00 00 b8  
  04 00 00 00 cd 80 b8 01   00 00 00 cd 80 00 00 00  
  00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00  
 *                                                   
  00 00 00 00 00 00 00 00   00 00 00 00              
",
            );
    }
}

mod base {
    use super::hexyl;
    use super::PrettyAssert;

    #[test]
    fn base2() {
        hexyl()
            .arg("ascii")
            .arg("--plain")
            .arg("--base=binary")
            .assert()
            .success()
            .pretty_stdout(
                "  00110000 00110001 00110010 00110011 00110100 00110101 00110110 00110111  \n  \
                   00111000 00111001 01100001 01100010 01100011 01100100 01100101 00001010  \n",
            );
    }
}

mod character_table {
    use super::hexyl;
    use super::PrettyAssert;

    #[test]
    fn ascii() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--color=never")
            .arg("--character-table=ascii")
            .assert()
            .success()
            .pretty_stdout(
                "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 7f 45 4c 46 02 01 01 00 ┊ 00 00 00 00 00 00 00 00 │.ELF....┊........│
│00000010│ 02 00 3e 00 01 00 00 00 ┊ 00 10 40 00 00 00 00 00 │..>.....┊..@.....│
│00000020│ 40 00 00 00 00 00 00 00 ┊ 28 20 00 00 00 00 00 00 │@.......┊( ......│
│00000030│ 00 00 00 00 40 00 38 00 ┊ 03 00 40 00 04 00 03 00 │....@.8.┊..@.....│
│00000040│ 01 00 00 00 04 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│00000050│ 00 00 40 00 00 00 00 00 ┊ 00 00 40 00 00 00 00 00 │..@.....┊..@.....│
│00000060│ e8 00 00 00 00 00 00 00 ┊ e8 00 00 00 00 00 00 00 │........┊........│
│00000070│ 00 10 00 00 00 00 00 00 ┊ 01 00 00 00 05 00 00 00 │........┊........│
│00000080│ 00 10 00 00 00 00 00 00 ┊ 00 10 40 00 00 00 00 00 │........┊..@.....│
│00000090│ 00 10 40 00 00 00 00 00 ┊ 1d 00 00 00 00 00 00 00 │..@.....┊........│
│000000a0│ 1d 00 00 00 00 00 00 00 ┊ 00 10 00 00 00 00 00 00 │........┊........│
│000000b0│ 01 00 00 00 06 00 00 00 ┊ 00 20 00 00 00 00 00 00 │........┊. ......│
│000000c0│ 00 20 40 00 00 00 00 00 ┊ 00 20 40 00 00 00 00 00 │. @.....┊. @.....│
│000000d0│ 0e 00 00 00 00 00 00 00 ┊ 0e 00 00 00 00 00 00 00 │........┊........│
│000000e0│ 00 10 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│000000f0│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│*       │                         ┊                         │        ┊        │
│00001000│ ba 0e 00 00 00 b9 00 20 ┊ 40 00 bb 01 00 00 00 b8 │....... ┊@.......│
│00001010│ 04 00 00 00 cd 80 b8 01 ┊ 00 00 00 cd 80 00 00 00 │........┊........│
│00001020│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│*       │                         ┊                         │        ┊        │
│00002000│ 48 65 6c 6c 6f 2c 20 77 ┊ 6f 72 6c 64 21 0a 00 2e │Hello, w┊orld!...│
│00002010│ 73 68 73 74 72 74 61 62 ┊ 00 2e 74 65 78 74 00 2e │shstrtab┊..text..│
│00002020│ 64 61 74 61 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │data....┊........│
│00002030│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│*       │                         ┊                         │        ┊        │
│00002060│ 00 00 00 00 00 00 00 00 ┊ 0b 00 00 00 01 00 00 00 │........┊........│
│00002070│ 06 00 00 00 00 00 00 00 ┊ 00 10 40 00 00 00 00 00 │........┊..@.....│
│00002080│ 00 10 00 00 00 00 00 00 ┊ 1d 00 00 00 00 00 00 00 │........┊........│
│00002090│ 00 00 00 00 00 00 00 00 ┊ 10 00 00 00 00 00 00 00 │........┊........│
│000020a0│ 00 00 00 00 00 00 00 00 ┊ 11 00 00 00 01 00 00 00 │........┊........│
│000020b0│ 03 00 00 00 00 00 00 00 ┊ 00 20 40 00 00 00 00 00 │........┊. @.....│
│000020c0│ 00 20 00 00 00 00 00 00 ┊ 0e 00 00 00 00 00 00 00 │. ......┊........│
│000020d0│ 00 00 00 00 00 00 00 00 ┊ 04 00 00 00 00 00 00 00 │........┊........│
│000020e0│ 00 00 00 00 00 00 00 00 ┊ 01 00 00 00 03 00 00 00 │........┊........│
│000020f0│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│00002100│ 0e 20 00 00 00 00 00 00 ┊ 17 00 00 00 00 00 00 00 │. ......┊........│
│00002110│ 00 00 00 00 00 00 00 00 ┊ 01 00 00 00 00 00 00 00 │........┊........│
│00002120│ 00 00 00 00 00 00 00 00 ┊                         │........┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
",
            );
    }

    #[test]
    fn codepage_437() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--color=never")
            .arg("--character-table=codepage-437")
            .assert()
            .success()
            .pretty_stdout(
                "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 7f 45 4c 46 02 01 01 00 ┊ 00 00 00 00 00 00 00 00 │⌂ELF☻☺☺⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│00000010│ 02 00 3e 00 01 00 00 00 ┊ 00 10 40 00 00 00 00 00 │☻⋄>⋄☺⋄⋄⋄┊⋄►@⋄⋄⋄⋄⋄│
│00000020│ 40 00 00 00 00 00 00 00 ┊ 28 20 00 00 00 00 00 00 │@⋄⋄⋄⋄⋄⋄⋄┊( ⋄⋄⋄⋄⋄⋄│
│00000030│ 00 00 00 00 40 00 38 00 ┊ 03 00 40 00 04 00 03 00 │⋄⋄⋄⋄@⋄8⋄┊♥⋄@⋄♦⋄♥⋄│
│00000040│ 01 00 00 00 04 00 00 00 ┊ 00 00 00 00 00 00 00 00 │☺⋄⋄⋄♦⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│00000050│ 00 00 40 00 00 00 00 00 ┊ 00 00 40 00 00 00 00 00 │⋄⋄@⋄⋄⋄⋄⋄┊⋄⋄@⋄⋄⋄⋄⋄│
│00000060│ e8 00 00 00 00 00 00 00 ┊ e8 00 00 00 00 00 00 00 │Φ⋄⋄⋄⋄⋄⋄⋄┊Φ⋄⋄⋄⋄⋄⋄⋄│
│00000070│ 00 10 00 00 00 00 00 00 ┊ 01 00 00 00 05 00 00 00 │⋄►⋄⋄⋄⋄⋄⋄┊☺⋄⋄⋄♣⋄⋄⋄│
│00000080│ 00 10 00 00 00 00 00 00 ┊ 00 10 40 00 00 00 00 00 │⋄►⋄⋄⋄⋄⋄⋄┊⋄►@⋄⋄⋄⋄⋄│
│00000090│ 00 10 40 00 00 00 00 00 ┊ 1d 00 00 00 00 00 00 00 │⋄►@⋄⋄⋄⋄⋄┊↔⋄⋄⋄⋄⋄⋄⋄│
│000000a0│ 1d 00 00 00 00 00 00 00 ┊ 00 10 00 00 00 00 00 00 │↔⋄⋄⋄⋄⋄⋄⋄┊⋄►⋄⋄⋄⋄⋄⋄│
│000000b0│ 01 00 00 00 06 00 00 00 ┊ 00 20 00 00 00 00 00 00 │☺⋄⋄⋄♠⋄⋄⋄┊⋄ ⋄⋄⋄⋄⋄⋄│
│000000c0│ 00 20 40 00 00 00 00 00 ┊ 00 20 40 00 00 00 00 00 │⋄ @⋄⋄⋄⋄⋄┊⋄ @⋄⋄⋄⋄⋄│
│000000d0│ 0e 00 00 00 00 00 00 00 ┊ 0e 00 00 00 00 00 00 00 │♫⋄⋄⋄⋄⋄⋄⋄┊♫⋄⋄⋄⋄⋄⋄⋄│
│000000e0│ 00 10 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄►⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│000000f0│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│*       │                         ┊                         │        ┊        │
│00001000│ ba 0e 00 00 00 b9 00 20 ┊ 40 00 bb 01 00 00 00 b8 │║♫⋄⋄⋄╣⋄ ┊@⋄╗☺⋄⋄⋄╕│
│00001010│ 04 00 00 00 cd 80 b8 01 ┊ 00 00 00 cd 80 00 00 00 │♦⋄⋄⋄═Ç╕☺┊⋄⋄⋄═Ç⋄⋄⋄│
│00001020│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│*       │                         ┊                         │        ┊        │
│00002000│ 48 65 6c 6c 6f 2c 20 77 ┊ 6f 72 6c 64 21 0a 00 2e │Hello, w┊orld!◙⋄.│
│00002010│ 73 68 73 74 72 74 61 62 ┊ 00 2e 74 65 78 74 00 2e │shstrtab┊⋄.text⋄.│
│00002020│ 64 61 74 61 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │data⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│00002030│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│*       │                         ┊                         │        ┊        │
│00002060│ 00 00 00 00 00 00 00 00 ┊ 0b 00 00 00 01 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊♂⋄⋄⋄☺⋄⋄⋄│
│00002070│ 06 00 00 00 00 00 00 00 ┊ 00 10 40 00 00 00 00 00 │♠⋄⋄⋄⋄⋄⋄⋄┊⋄►@⋄⋄⋄⋄⋄│
│00002080│ 00 10 00 00 00 00 00 00 ┊ 1d 00 00 00 00 00 00 00 │⋄►⋄⋄⋄⋄⋄⋄┊↔⋄⋄⋄⋄⋄⋄⋄│
│00002090│ 00 00 00 00 00 00 00 00 ┊ 10 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊►⋄⋄⋄⋄⋄⋄⋄│
│000020a0│ 00 00 00 00 00 00 00 00 ┊ 11 00 00 00 01 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊◄⋄⋄⋄☺⋄⋄⋄│
│000020b0│ 03 00 00 00 00 00 00 00 ┊ 00 20 40 00 00 00 00 00 │♥⋄⋄⋄⋄⋄⋄⋄┊⋄ @⋄⋄⋄⋄⋄│
│000020c0│ 00 20 00 00 00 00 00 00 ┊ 0e 00 00 00 00 00 00 00 │⋄ ⋄⋄⋄⋄⋄⋄┊♫⋄⋄⋄⋄⋄⋄⋄│
│000020d0│ 00 00 00 00 00 00 00 00 ┊ 04 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊♦⋄⋄⋄⋄⋄⋄⋄│
│000020e0│ 00 00 00 00 00 00 00 00 ┊ 01 00 00 00 03 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊☺⋄⋄⋄♥⋄⋄⋄│
│000020f0│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│00002100│ 0e 20 00 00 00 00 00 00 ┊ 17 00 00 00 00 00 00 00 │♫ ⋄⋄⋄⋄⋄⋄┊↨⋄⋄⋄⋄⋄⋄⋄│
│00002110│ 00 00 00 00 00 00 00 00 ┊ 01 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊☺⋄⋄⋄⋄⋄⋄⋄│
│00002120│ 00 00 00 00 00 00 00 00 ┊                         │⋄⋄⋄⋄⋄⋄⋄⋄┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
",
            );
    }

    #[test]
    fn codepage_1047() {
        hexyl()
            .arg("hello_world_elf64")
            .arg("--color=never")
            .arg("--character-table=codepage-1047")
            .assert()
            .success()
            .pretty_stdout(
                "┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 7f 45 4c 46 02 01 01 00 ┊ 00 00 00 00 00 00 00 00 │..<.....┊........│
│00000010│ 02 00 3e 00 01 00 00 00 ┊ 00 10 40 00 00 00 00 00 │........┊.. .....│
│00000020│ 40 00 00 00 00 00 00 00 ┊ 28 20 00 00 00 00 00 00 │ .......┊........│
│00000030│ 00 00 00 00 40 00 38 00 ┊ 03 00 40 00 04 00 03 00 │.... ...┊.. .....│
│00000040│ 01 00 00 00 04 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│00000050│ 00 00 40 00 00 00 00 00 ┊ 00 00 40 00 00 00 00 00 │.. .....┊.. .....│
│00000060│ e8 00 00 00 00 00 00 00 ┊ e8 00 00 00 00 00 00 00 │Y.......┊Y.......│
│00000070│ 00 10 00 00 00 00 00 00 ┊ 01 00 00 00 05 00 00 00 │........┊........│
│00000080│ 00 10 00 00 00 00 00 00 ┊ 00 10 40 00 00 00 00 00 │........┊.. .....│
│00000090│ 00 10 40 00 00 00 00 00 ┊ 1d 00 00 00 00 00 00 00 │.. .....┊........│
│000000a0│ 1d 00 00 00 00 00 00 00 ┊ 00 10 00 00 00 00 00 00 │........┊........│
│000000b0│ 01 00 00 00 06 00 00 00 ┊ 00 20 00 00 00 00 00 00 │........┊........│
│000000c0│ 00 20 40 00 00 00 00 00 ┊ 00 20 40 00 00 00 00 00 │.. .....┊.. .....│
│000000d0│ 0e 00 00 00 00 00 00 00 ┊ 0e 00 00 00 00 00 00 00 │........┊........│
│000000e0│ 00 10 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│000000f0│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│*       │                         ┊                         │        ┊        │
│00001000│ ba 0e 00 00 00 b9 00 20 ┊ 40 00 bb 01 00 00 00 b8 │[.......┊ .].....│
│00001010│ 04 00 00 00 cd 80 b8 01 ┊ 00 00 00 cd 80 00 00 00 │........┊........│
│00001020│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│*       │                         ┊                         │        ┊        │
│00002000│ 48 65 6c 6c 6f 2c 20 77 ┊ 6f 72 6c 64 21 0a 00 2e │..%%?...┊?.%.....│
│00002010│ 73 68 73 74 72 74 61 62 ┊ 00 2e 74 65 78 74 00 2e │....../.┊........│
│00002020│ 64 61 74 61 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │././....┊........│
│00002030│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│*       │                         ┊                         │        ┊        │
│00002060│ 00 00 00 00 00 00 00 00 ┊ 0b 00 00 00 01 00 00 00 │........┊........│
│00002070│ 06 00 00 00 00 00 00 00 ┊ 00 10 40 00 00 00 00 00 │........┊.. .....│
│00002080│ 00 10 00 00 00 00 00 00 ┊ 1d 00 00 00 00 00 00 00 │........┊........│
│00002090│ 00 00 00 00 00 00 00 00 ┊ 10 00 00 00 00 00 00 00 │........┊........│
│000020a0│ 00 00 00 00 00 00 00 00 ┊ 11 00 00 00 01 00 00 00 │........┊........│
│000020b0│ 03 00 00 00 00 00 00 00 ┊ 00 20 40 00 00 00 00 00 │........┊.. .....│
│000020c0│ 00 20 00 00 00 00 00 00 ┊ 0e 00 00 00 00 00 00 00 │........┊........│
│000020d0│ 00 00 00 00 00 00 00 00 ┊ 04 00 00 00 00 00 00 00 │........┊........│
│000020e0│ 00 00 00 00 00 00 00 00 ┊ 01 00 00 00 03 00 00 00 │........┊........│
│000020f0│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │........┊........│
│00002100│ 0e 20 00 00 00 00 00 00 ┊ 17 00 00 00 00 00 00 00 │........┊........│
│00002110│ 00 00 00 00 00 00 00 00 ┊ 01 00 00 00 00 00 00 00 │........┊........│
│00002120│ 00 00 00 00 00 00 00 00 ┊                         │........┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
",
            );
    }
}

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);
    }
}
Download .txt
gitextract_k919d0ul/

├── .github/
│   └── workflows/
│       └── CICD.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── doc/
│   ├── hexyl.1.md
│   └── sponsors.md
├── examples/
│   └── simple.rs
├── src/
│   ├── colors.rs
│   ├── input.rs
│   ├── lib.rs
│   ├── main.rs
│   └── tests.rs
└── tests/
    ├── examples/
    │   ├── .gitattributes
    │   ├── ascii
    │   ├── empty
    │   └── hello_world_elf64
    └── integration_tests.rs
Download .txt
SYMBOL INDEX (162 symbols across 7 files)

FILE: examples/simple.rs
  function main (line 5) | fn main() {

FILE: src/colors.rs
  constant COLOR_RESET (line 17) | pub const COLOR_RESET: &str = colors::Default::ANSI_FG;
  function init_color (line 19) | fn init_color(name: &str, default_ansi: AnsiColors) -> String {
  constant COLOR_NULL_RGB (line 38) | pub const COLOR_NULL_RGB: &[u8] = &rgb_bytes(100, 100, 100);
  constant COLOR_DEL (line 40) | pub const COLOR_DEL: &[u8] = &rgb_bytes(64, 128, 0);
  constant COLOR_GRADIENT_NONASCII (line 42) | pub const COLOR_GRADIENT_NONASCII: [[u8; 19]; 128] =
  constant COLOR_GRADIENT_ASCII_NONPRINTABLE (line 45) | pub const COLOR_GRADIENT_ASCII_NONPRINTABLE: [[u8; 19]; 31] =
  constant COLOR_GRADIENT_ASCII_PRINTABLE (line 48) | pub const COLOR_GRADIENT_ASCII_PRINTABLE: [[u8; 19]; 95] =
  function as_dec (line 51) | const fn as_dec(byte: u8) -> [u8; 3] {
  function rgb_bytes (line 59) | const fn rgb_bytes(r: u8, g: u8, b: u8) -> [u8; 19] {
  function generate_color_gradient (line 80) | const fn generate_color_gradient<const N: usize>(stops: &[(u8, u8, u8, f...
  constant CP437 (line 114) | pub const CP437: [char; 256] = [
  constant CP1047 (line 153) | pub const CP1047: [char; 256] = [

FILE: src/input.rs
  type Input (line 4) | pub enum Input<'a> {
  method read (line 10) | fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
  method seek (line 19) | fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
  function into_inner (line 58) | pub fn into_inner(self) -> Box<dyn Read + 'a> {

FILE: src/lib.rs
  type Base (line 11) | pub enum Base {
  type ByteCategory (line 19) | pub enum ByteCategory {
  type IncludeMode (line 27) | pub enum IncludeMode {
  type CharacterTable (line 36) | pub enum CharacterTable {
  type ColorScheme (line 60) | pub enum ColorScheme {
  type Endianness (line 74) | pub enum Endianness {
  type Squeezer (line 84) | enum Squeezer {
  type Byte (line 92) | struct Byte(u8);
    method category (line 95) | fn category(self) -> ByteCategory {
    method color (line 109) | fn color(self, color_scheme: ColorScheme) -> &'static [u8] {
    method as_char (line 135) | fn as_char(self, character_table: CharacterTable) -> char {
  type BorderElements (line 191) | struct BorderElements {
  type BorderStyle (line 199) | pub enum BorderStyle {
    method header_elems (line 212) | fn header_elems(&self) -> Option<BorderElements> {
    method footer_elems (line 230) | fn footer_elems(&self) -> Option<BorderElements> {
    method outer_sep (line 248) | fn outer_sep(&self) -> char {
    method inner_sep (line 256) | fn inner_sep(&self) -> char {
  type PrinterBuilder (line 265) | pub struct PrinterBuilder<'a, Writer: Write> {
  function new (line 282) | pub fn new(writer: &'a mut Writer) -> Self {
  function show_color (line 300) | pub fn show_color(mut self, show_color: bool) -> Self {
  function show_char_panel (line 305) | pub fn show_char_panel(mut self, show_char_panel: bool) -> Self {
  function show_position_panel (line 310) | pub fn show_position_panel(mut self, show_position_panel: bool) -> Self {
  function with_border_style (line 315) | pub fn with_border_style(mut self, border_style: BorderStyle) -> Self {
  function enable_squeezing (line 320) | pub fn enable_squeezing(mut self, enable: bool) -> Self {
  function num_panels (line 325) | pub fn num_panels(mut self, num: u64) -> Self {
  function group_size (line 330) | pub fn group_size(mut self, num: u8) -> Self {
  function with_base (line 335) | pub fn with_base(mut self, base: Base) -> Self {
  function endianness (line 340) | pub fn endianness(mut self, endianness: Endianness) -> Self {
  function character_table (line 345) | pub fn character_table(mut self, character_table: CharacterTable) -> Self {
  function include_mode (line 350) | pub fn include_mode(mut self, include: IncludeMode) -> Self {
  function color_scheme (line 355) | pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
  function build (line 360) | pub fn build(self) -> Printer<'a, Writer> {
  type Printer (line 404) | pub struct Printer<'a, Writer: Write> {
  function display_offset (line 435) | pub fn display_offset(&mut self, display_offset: u64) -> &mut Self {
  function panel_sz (line 440) | fn panel_sz(&self) -> usize {
  function write_border (line 448) | fn write_border(&mut self, border_elements: BorderElements) -> io::Resul...
  function print_header (line 483) | pub fn print_header(&mut self) -> io::Result<()> {
  function print_footer (line 490) | pub fn print_footer(&mut self) -> io::Result<()> {
  function print_position_panel (line 497) | fn print_position_panel(&mut self) -> io::Result<()> {
  function print_char (line 541) | fn print_char(&mut self, i: u64) -> io::Result<()> {
  function print_char_panel (line 585) | pub fn print_char_panel(&mut self) -> io::Result<()> {
  function print_byte (line 592) | fn print_byte(&mut self, i: usize, b: u8) -> io::Result<()> {
  function reorder_buffer_to_little_endian (line 651) | fn reorder_buffer_to_little_endian(&self, buf: &mut [u8]) {
  function print_bytes (line 663) | pub fn print_bytes(&mut self) -> io::Result<()> {
  function print_all (line 678) | pub fn print_all<Reader: Read>(&mut self, reader: Reader) -> io::Result<...
  function print_bytes_in_include_style (line 856) | fn print_bytes_in_include_style<Reader: Read>(
  function assert_print_all_output (line 900) | fn assert_print_all_output<Reader: Read>(input: Reader, expected_string:...
  function empty_file_passes (line 924) | fn empty_file_passes() {
  function short_input_passes (line 936) | fn short_input_passes() {
  function display_offset (line 948) | fn display_offset() {
  function multiple_panels (line 982) | fn multiple_panels() {
  function squeeze_works (line 1017) | fn squeeze_works() {
  function squeeze_nonzero (line 1031) | fn squeeze_nonzero() {
  function squeeze_multiple_panels (line 1045) | fn squeeze_multiple_panels() {
  function display_offset_in_last_line (line 1080) | fn display_offset_in_last_line() {
  function include_mode_from_file (line 1093) | fn include_mode_from_file() {
  function include_mode_from_stdin (line 1125) | fn include_mode_from_stdin() {

FILE: src/main.rs
  constant DEFAULT_BLOCK_SIZE (line 32) | const DEFAULT_BLOCK_SIZE: i64 = 512;
  constant LENGTH_HELP_TEXT (line 34) | const LENGTH_HELP_TEXT: &str = "Only read N bytes from the input. The N ...
  constant SKIP_HELP_TEXT (line 40) | const SKIP_HELP_TEXT: &str = "Skip the first N bytes of the input. The N...
  constant BLOCK_SIZE_HELP_TEXT (line 44) | const BLOCK_SIZE_HELP_TEXT: &str = "Sets the size of the `block` unit to...
  constant DISPLAY_OFFSET_HELP_TEXT (line 47) | const DISPLAY_OFFSET_HELP_TEXT: &str = "Add N bytes to the displayed fil...
  constant TERMINAL_WIDTH_HELP_TEXT (line 52) | const TERMINAL_WIDTH_HELP_TEXT: &str = "Sets the number of terminal colu...
  constant STYLES (line 59) | const STYLES: Styles = Styles::styled()
  type Opt (line 67) | struct Opt {
  type ColorWhen (line 223) | enum ColorWhen {
  type GroupSize (line 239) | enum GroupSize {
  function from (line 259) | fn from(number: GroupSize) -> Self {
  function run (line 269) | fn run() -> Result<()> {
  function main (line 521) | fn main() {
  type NonNegativeI64 (line 536) | pub struct NonNegativeI64(i64);
    method new (line 539) | pub fn new(x: i64) -> Option<Self> {
    method into_inner (line 547) | pub fn into_inner(self) -> i64 {
  function from (line 553) | fn from(x: NonNegativeI64) -> u64 {
  function print_color_table (line 559) | fn print_color_table() -> io::Result<()> {
  type PositiveI64 (line 596) | pub struct PositiveI64(i64);
    method new (line 599) | pub fn new(x: i64) -> Option<Self> {
    method into_inner (line 607) | pub fn into_inner(self) -> i64 {
  function from (line 613) | fn from(x: PositiveI64) -> u64 {
  type Unit (line 620) | enum Unit {
    method get_multiplier (line 637) | const fn get_multiplier(self) -> i64 {
  constant HEX_PREFIX (line 656) | const HEX_PREFIX: &str = "0x";
  type ByteOffsetKind (line 659) | enum ByteOffsetKind {
  type ByteOffset (line 666) | struct ByteOffset {
    method assume_forward_offset_from_start (line 678) | fn assume_forward_offset_from_start(
  type NegativeOffsetSpecifiedError (line 675) | struct NegativeOffsetSpecifiedError;
  type ByteOffsetParseError (line 692) | enum ByteOffsetParseError {
  function parse_byte_offset (line 714) | fn parse_byte_offset(n: &str, block_size: PositiveI64) -> Result<ByteOff...
  function extract_num_and_unit_from (line 749) | fn extract_num_and_unit_from(n: &str) -> Result<(i64, Unit), ByteOffsetP...
  function process_sign_of (line 795) | fn process_sign_of(n: &str) -> Result<(&str, ByteOffsetKind), ByteOffset...
  function try_parse_as_hex_number (line 819) | fn try_parse_as_hex_number(n: &str) -> Option<Result<i64, ByteOffsetPars...

FILE: src/tests.rs
  function unit_multipliers (line 4) | fn unit_multipliers() {
  function test_process_sign (line 18) | fn test_process_sign() {
  function test_parse_as_hex (line 30) | fn test_parse_as_hex() {
  function extract_num_and_unit (line 38) | fn extract_num_and_unit() {
  function test_parse_byte_offset (line 66) | fn test_parse_byte_offset() {

FILE: tests/integration_tests.rs
  function hexyl (line 3) | fn hexyl() -> Command {
  type PrettyAssert (line 8) | trait PrettyAssert<S>
    method pretty_stdout (line 12) | fn pretty_stdout(self, other: S);
  function pretty_stdout (line 21) | fn pretty_stdout(self, other: S) {
  function can_print_simple_ascii_file (line 33) | fn can_print_simple_ascii_file() {
  function can_read_input_from_stdin (line 47) | fn can_read_input_from_stdin() {
  function fails_on_non_existing_input (line 61) | fn fails_on_non_existing_input() {
  function prints_warning_on_empty_content (line 66) | fn prints_warning_on_empty_content() {
  function length_restricts_output_size (line 84) | fn length_restricts_output_size() {
  function fail_if_length_and_bytes_options_are_used_simultaneously (line 100) | fn fail_if_length_and_bytes_options_are_used_simultaneously() {
  function fail_if_length_and_count_options_are_used_simultaneously (line 110) | fn fail_if_length_and_count_options_are_used_simultaneously() {
  function fail_if_bytes_and_count_options_are_used_simultaneously (line 124) | fn fail_if_bytes_and_count_options_are_used_simultaneously() {
  function basic (line 138) | fn basic() {
  function prints_warning_when_skipping_past_the_end (line 154) | fn prints_warning_when_skipping_past_the_end() {
  function negative_offset (line 169) | fn negative_offset() {
  function fails_if_negative_offset_is_too_large (line 185) | fn fails_if_negative_offset_is_too_large() {
  function basic (line 200) | fn basic() {
  function display_offset_and_skip (line 215) | fn display_offset_and_skip() {
  function fails_for_zero_or_negative_blocksize (line 236) | fn fails_for_zero_or_negative_blocksize() {
  function plain (line 255) | fn plain() {
  function no_chars (line 265) | fn no_chars() {
  function no_position (line 280) | fn no_position() {
  function group_2_bytes_be (line 300) | fn group_2_bytes_be() {
  function group_2_bytes_le (line 315) | fn group_2_bytes_le() {
  function group_4_bytes_be (line 331) | fn group_4_bytes_be() {
  function group_4_bytes_le (line 346) | fn group_4_bytes_le() {
  function group_8_bytes_be (line 362) | fn group_8_bytes_be() {
  function group_8_bytes_le (line 377) | fn group_8_bytes_le() {
  function group_size_plain (line 393) | fn group_size_plain() {
  function group_size_fill_space (line 405) | fn group_size_fill_space() {
  function group_size_invalid (line 420) | fn group_size_invalid() {
  function squeeze_no_chars (line 430) | fn squeeze_no_chars() {
  function squeeze_no_chars_one_panel (line 454) | fn squeeze_no_chars_one_panel() {
  function squeeze_no_position (line 481) | fn squeeze_no_position() {
  function squeeze_no_position_one_panel (line 505) | fn squeeze_no_position_one_panel() {
  function squeeze_odd_panels_remainder_bytes (line 532) | fn squeeze_odd_panels_remainder_bytes() {
  function squeeze_plain (line 557) | fn squeeze_plain() {
  function squeeze_plain_remainder (line 580) | fn squeeze_plain_remainder() {
  function base2 (line 608) | fn base2() {
  function ascii (line 627) | fn ascii() {
  function codepage_437 (line 681) | fn codepage_437() {
  function codepage_1047 (line 735) | fn codepage_1047() {
  type ColorMap (line 818) | struct ColorMap {
    method from (line 824) | fn from(text_map: &'static str) -> Self {
    method with (line 831) | fn with<C: Color>(&mut self, c: char) -> &mut Self {
    method colorize (line 836) | fn colorize(&self, input: &str) -> String {
  function hex_colors (line 854) | fn hex_colors() {
  function binary_colors (line 890) | fn binary_colors() {
  function groupsize_colors (line 928) | fn groupsize_colors() {
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (174K chars).
[
  {
    "path": ".github/workflows/CICD.yml",
    "chars": 13387,
    "preview": "name: CICD\n\nenv:\n  CICD_INTERMEDIATES_DIR: \"_cicd-intermediates\"\n  MSRV_FEATURES: \"\"\n\non:\n  workflow_dispatch:\n  pull_re"
  },
  {
    "path": ".gitignore",
    "chars": 46,
    "preview": "/target\n**/*.rs.bk\n\n# Generated files\nhexyl.1\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 7975,
    "preview": "# v0.17.0\n\n## Features\n\n- Add option to output result in C include file style, see #242 (@wpcwzy)\n- Add `--color-scheme`"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 983,
    "preview": "# Contributing\n\nThank you for considering to contribute to `hexyl`!\n\n\n\n## Add an entry to the changelog\n\nIf your contrib"
  },
  {
    "path": "Cargo.toml",
    "chars": 751,
    "preview": "[package]\nauthors = [\"David Peter <mail@david-peter.de>\"]\ncategories = [\"command-line-utilities\"]\nkeywords = [\"hex\", \"vi"
  },
  {
    "path": "LICENSE-APACHE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "LICENSE-MIT",
    "chars": 1023,
    "preview": "Permission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentati"
  },
  {
    "path": "README.md",
    "chars": 5387,
    "preview": "![](doc/logo.svg)\n\n[![CICD](https://github.com/sharkdp/hexyl/actions/workflows/CICD.yml/badge.svg)](https://github.com/s"
  },
  {
    "path": "doc/hexyl.1.md",
    "chars": 4400,
    "preview": "% HEXYL(1) hexyl 0.12.0 | General Commands Manual\n%\n% 2022-12-05\n\n# NAME\n\nhexyl - a command-line hex viewer\n\n# SYNOPSIS\n"
  },
  {
    "path": "doc/sponsors.md",
    "chars": 628,
    "preview": "## Sponsors\n\n`hexyl` development is sponsored by many individuals and companies. Thank you very much!\n\nPlease note, that"
  },
  {
    "path": "examples/simple.rs",
    "chars": 692,
    "preview": "use std::io;\n\nuse hexyl::{BorderStyle, PrinterBuilder};\n\nfn main() {\n    let input = [\n        0x89, 0x50, 0x4e, 0x47, 0"
  },
  {
    "path": "src/colors.rs",
    "chars": 7695,
    "preview": "use owo_colors::{colors, AnsiColors, Color, DynColors, OwoColorize};\nuse std::str::FromStr;\nuse std::sync::LazyLock;\n\npu"
  },
  {
    "path": "src/input.rs",
    "chars": 1911,
    "preview": "use std::fs;\nuse std::io::{self, copy, sink, Read, Seek, SeekFrom};\n\npub enum Input<'a> {\n    File(fs::File),\n    Stdin("
  },
  {
    "path": "src/lib.rs",
    "chars": 40308,
    "preview": "pub(crate) mod colors;\npub(crate) mod input;\n\npub use colors::*;\npub use input::*;\n\nuse std::io::{self, BufReader, Read,"
  },
  {
    "path": "src/main.rs",
    "chars": 26242,
    "preview": "use std::fs::File;\nuse std::io::{self, prelude::*, BufWriter, SeekFrom};\nuse std::num::{NonZeroI64, NonZeroU64};\nuse std"
  },
  {
    "path": "src/tests.rs",
    "chars": 5996,
    "preview": "use super::*;\n\n#[test]\nfn unit_multipliers() {\n    use Unit::*;\n    assert_eq!(Kilobyte.get_multiplier(), 1000 * Byte.ge"
  },
  {
    "path": "tests/examples/.gitattributes",
    "chars": 18,
    "preview": "ascii text eol=lf\n"
  },
  {
    "path": "tests/examples/ascii",
    "chars": 16,
    "preview": "0123456789abcde\n"
  },
  {
    "path": "tests/examples/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/integration_tests.rs",
    "chars": 37986,
    "preview": "use assert_cmd::Command;\n\nfn hexyl() -> Command {\n    let mut cmd = Command::new(assert_cmd::cargo_bin!(\"hexyl\"));\n    c"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the sharkdp/hexyl GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (162.9 KB), approximately 47.9k tokens, and a symbol index with 162 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!