A Vim plugin that turns the classic :%!xxd hex-dump workflow into a
small, reliable hex editor — with live highlighting of the byte pair
under the cursor in both the HEX and the ASCII column, byte-exact
cursor mapping between the views, :w that writes only the page you are
looking at, and forgiving editing where the offset and ASCII columns are
purely decorative.
It is not the best hex editor in the world, and does not try to be. But it
is always a single command away wherever you already
have Vim — no system install, no package manager, nothing to get approved.
Everything runs on xxd, which ships with Vim itself, and portable
VimScript: no sed, tr, dd or anything else, so it behaves the same
on Linux, macOS and the BSDs, on native Windows (where xxd.exe is found
inside the Vim installation even when it is not on PATH) and inside WSL —
with one platform note about files over 2 GiB on native Windows, below.
And it is good enough for real work on real, even very big, files: because it shows one page at a time and writes one page at a time, a file that does not fit in memory — a disk image, a core dump, a database — is no different from a small one. Editing an 8 TiB file costs the same ~20 MiB of memory as editing an 8 KiB one.
A note about
xxdon native Windows.xxdkeeps its seek offset in a Clong, which is 32 bits there, so past 2 GiB it reads and writes the wrong place. Hexpair does the seeking with PowerShell beyond that point instead, and everything works — reading, searching, comparing, overwriting, growing, shrinking,:w {file}. What changes is the cost: a process start per operation.This is a limitation of
xxdon one platform, not of hexpair and not of the file. On Linux, macOS, the BSDs and inside WSL alongis 64 bits andxxdseeks natively, so files of any size work the ordinary way — the ceiling is 2⁶³−1 bytes, eight exbibytes, which is not a limit anyone is going to meet. See 2 GiB Limit on Windows.
The hexpair name: hex and text, always paired. Within a line — the byte under the cursor and its character light up together, whichever column you are in, one byte or a whole Visual selection. Between the views — the page as a hex dump and the same page as raw text, toggled with the cursor left on the same byte.
Project page: https://github.com/michal-ruzicka/hexpair — source code, releases and issue tracker.
Releases: https://github.com/michal-ruzicka/hexpair/releases
On vim.org: https://www.vim.org/scripts/script.php?script_id=6194 — the same plugin in the smaller package described under Installation; the releases above carry it too, beside the complete one.
Support: If you find this plugin useful, consider supporting its development.
-
https://github.com/sponsors/michal-ruzicka — GitHub Sponsors (GitHub account needed).
-
https://ko-fi.com/michal_ruzicka — Buy me a Coffee with no specific account needed, card payment is possible.
-
https://revolut.me/ruzicka_michal — Donate me via Revolut, debit/credit card or Apple Pay.
Install the latest release (it is worth verifying the signature first), source the two ready-made files, and you have all of it:
# Download and install – see https://github.com/michal-ruzicka/hexpair/releases/latest
curl -LO 'https://github.com/michal-ruzicka/hexpair/releases/download/vX.Y.Z/hexpair.vX.Y.Z.tar'
mkdir -p ~/.vim/pack/plugins/start
tar xvf hexpair.vX.Y.Z.tar -C ~/.vim/pack/plugins/start/ # install it into your own Vim configuration directory
vim -c 'helptags ALL' -c 'q' # regenerate helptags for your installation
echo "let mapleader = ','" >> ~/.vimrc # your <Leader> key (`\` is the default); must come first
echo 'runtime pack/*/start/hexpair/hexpair.vimrc' >> ~/.vimrc # load recommended hexpair keybindings on Vim start
echo 'source ~/.vim/pack/plugins/start/hexpair/hexpair.bashrc' >> ~/.bashrc # load the vimhex/vimhexdiff shell functions on Bash start
# Start a new Bash for the shell functions, and a new Vim for the mappings:
# hex editor
vimhex # no parameters: print the usage and stop
vimhex bigfile.bin # the first page; also 3, '$', '$-5', @0x4a2000
# hex diff
vimhexdiff # no parameters: print the usage and stop
vimhexdiff old.img new.img # both side by side, each marking what differs
# gvimhex and gvimhexdiff are those two again, in gVim rather than in the terminalAnd that is the whole of it, from a Vim that already has it:
" hexpair defines NO keys of its own - hexpair.vimrc is one answer to
" which keys, the maintainer's, and it never takes a key you have already
" mapped. <Leader> is whatever 'mapleader' held when it was sourced.
let mapleader = ',' " i.e. in Normal mode type `,h` to toggle hex <-> text
" === Normal mode ====================================================
" Views
<Leader>h " hex page view <-> text page view
<Leader>u " back to the plain, unpaged buffer (a toggled file only)
<Leader>U " ... discarding the page's unwritten edits
<Leader>< " cursor to the HEX column, same byte
<Leader>> " cursor to the ASCII column, same byte
<Leader>- " cursor to the opposite column
<Leader>r " regenerate the offset and ASCII columns, without writing
" Moving around the file
<Leader>j " next page
<Leader>J " ... discarding unwritten edits
<Leader>k " previous page
<Leader>K " ... discarding unwritten edits
<Leader>g " ask which page: N, +N, -N, $, $-N
<Leader>G " ... discarding unwritten edits
<Leader>b " ask which byte: 1-based decimal or 0x..., +N, -N, $, $-N
<Leader>B " ... discarding unwritten edits
<Leader>? " where am I: page X of Y, the byte range, the size, the byte
<Leader>= " bring the other scroll-bound view onto the byte I am on
" Reading the bytes
<Leader>i " the bytes at the cursor as int8..64, float32/64, utf-8/16/32
<Leader>I " ask for a character, write its bytes in
<Leader>s " how many bytes the selection covers, and which (also in Visual)
" Marks – positions in the FILE, so a page turn does not disturb them
<Leader>ml " list them, with the page each falls on
<Leader>ms " set a mark: ask for a name, mark the byte under the cursor
<Leader>md " drop a mark: ask which one (TAB completes the names)
<Leader>mg " go to a mark: ask which one (TAB completes the names)
<Leader>mG " ... discarding unwritten edits
" Searching the whole file, not the page
<Leader>/ " ask for bytes to find ('?' matches any nibble)
<Leader>t " ask for text to find, as its bytes
<Leader>f " next match
<Leader>F " previous match
<Leader>c " stop marking the matches
" Your own unwritten edits
<Leader>e " go to the next run of bytes edited and not yet written
<Leader>E " ... the previous one
<Leader>M " stop marking them, and start again – after an insert or a
" delete every byte behind it is marked, which is correct and
" costs a comparison of the whole page on every keystroke
<Leader>d " what was here before you edited it (also in Visual)
" Comparing with the file :HexPairDiff named
<Leader>] " go to the next change – a run of differing bytes is one change
<Leader>[ " ... the previous one
<Leader>D " what the file you are comparing with has here (also in Visual)
<Leader>C " stop comparing, and clear the marking
" === Command mode - what takes an argument, so it has no key =========
:HPOpen[!] {file} [page] " page a file WITHOUT loading it; ! over a modified buffer
:HPDiff[!] [file] " compare with {file}, marking the bytes that differ
" (no [file]: say what with; ! stops comparing)
:HPReplace {bytes} " over the match under the cursor
:HPReplaceAllInPage {bytes} / {bytes} " over every match on this page
:HPSplit [page] " a second view of this file, e.g. :HPSplit +1
:HPVSplit [page] " ... vertically
" Short names for the :HexPair... commands below; same arguments and bang.
" HexPairStatus() goes in 'statusline' and reports "hex 3/349 @0x50a01"
" in a hex view and nothing anywhere else:
" set statusline=%f\ %h%w%m%r\ %{HexPairStatus()}%=%l,%c%V\ %PEverything below says the same at length: what each of those does, how paging keeps a file's size from mattering, what a write costs, and how to change any of it.
README.md is long, so here is its shape. Everything after Features is
the same ground as the Quick Start above, walked slowly.
- TL;DR: Quick Start
- Features
- Installation
- Usage
- Pages
- What it costs
- Requirements
- Contributing
- License
- Pair highlighting. In the hex view, the byte under the cursor is
highlighted in both columns: stand on a hex byte and the matching
ASCII character lights up, and vice versa. The cursor side gets a
subtle underline (
HexPairActive), the counterpart a prominent highlight (HexPairMirror) — so you always see at a glance which column you are in. A Visual selection is mirrored the same way: select a run of hex digits and the text it is lights up, select a run of text and the bytes it is light up. - Byte-exact cursor mapping. Every transition — into hex mode,
between the hex and text views, across a write, across a page turn —
keeps the cursor on the same byte, and entering hex mode opens on
the page that byte is on. The mapping stays
exact for files with a BOM, CRLF or even mixed CRLF/LF line endings,
and single-byte file encodings (
latin*,cp125x, ...). - Safe writing, scoped to the page.
:wconverts back to binary and writes just the page you are looking at — the file on disk always contains the real bytes, never the textual dump, and regions of the file you never looked at cannot be clobbered. Overwriting values patches in place at a cost independent of the file's size. - Forgiving editing. Only the HEX column matters: the offset and
ASCII columns are stripped before the reverse conversion, so you can
freely insert, delete or reorder lines — inserted lines need no
offset and no ASCII part, just hex pairs. After
:w, or on demand with:HexPairRefresh, the dump is regenerated and offsets become correct again automatically —:HexPairRefreshdoes this without writing to disk, and without affecting the'modified'flag. - Column navigation. Commands to jump between the HEX and ASCII representation of the byte under the cursor, or to swap to the opposite column.
- Search that knows the file, not the page.
:HexPairFind de ad be eflooks through the whole file a block at a time and lands on the byte it found, turning the page on the way;?stands for any nibble, and:HexPairFindTexttakes a string.:HexPairReplaceand:HexPairReplaceAllInPageput new bytes over what was found. - Changed bytes are visible, and walkable. Everything edited and not
yet written is marked in both columns (
HexPairModified), so an edit in a dump does not look exactly like everything around it, and:HexPairModifiedNext/:HexPairModifiedPrevmove between the runs of them the way:HexPairDiffNextmoves between changes against another file.:HexPairModifiedShowsays what was there before — at the cursor, or over a Visual selection — which is the one thing a marking cannot show, since the byte it covers is the new one. The marking groups link to your colour scheme's diff and search colours rather than naming colours of their own — with stock Vim and with every scheme it ships, that resolves to something readable on a light and a dark background alike, and:highlight HexPairModifiedsays what yours came out as. - Compare with another file.
:HexPairDiff other.binmarks every byte of the page that differs from the same offset ofother.bin, and:HexPairDiffNextwalks the whole file for the next disagreement.:HexPairDiffShowsays what that file actually holds at the cursor — or over a Visual selection — including when it holds nothing there, because it ends before this offset. That last case is one a marking cannot express: every byte of such a page differs, and the reason is not on screen. The bundledvimhexdiff a bopens both files side by side that way. - Marks that survive page turns.
:HexPairMark headerremembers a byte of the file, not a line of a buffer — and the byte it stands on is underlined on the page (HexPairMark), so a place worth coming back to is visible while you are there. - A data inspector.
:HexPairInspectreads the bytes at the cursor as the numbers they could be — 8, 16, 32 and 64 bits wide, unsigned and signed, little- and big-endian, plusfloat32andfloat64— which is the one question a hex dump cannot answer on its own. The bytes it read are marked on the page while you stay on them (HexPairInspect), so the report's first line does not have to be counted back off a line of forty-eight digits. - The bytes of a character, written in.
:HexPairInsertChar Šputsc5 a0in — or60 01, or01 60 00 00, depending ong:hexpair_insert_encoding. It is the inspector read backwards: that one says what the bytes at the cursor would be as utf-8, utf-16 and utf-32, and this one writes a character in exactly those. - A selection knows its size.
:HexPairSelectionsays how many bytes a Visual selection covers and which ones, in the same 1-based numbering:HexPairGoOffsettakes. - Statusline support.
HexPairStatus()putshex 3/349 @0x50a01in your statusline and returns nothing in buffers hexpair never touched, so one statusline works everywhere. - An optional column ruler.
g:hexpair_rulernumbers the byte columns above the dump. - Binary correctness. Opening a file without
-bis handled by an automatic:edit ++binreload, so the dump always shows the exact on-disk bytes (and a plain:wcannot silently re-encode a binary file). - Errors are refused, not guessed. A non-hex character in the hex
area, or an odd total number of hex digits, aborts
:wand hex-mode the switch to the text view with an error and the cursor parked on the offender — the file and the dump keep their previous content. Reloading with:ewhile in hex mode regenerates the dump from the fresh file content and stays in hex mode instead of breaking the view. - Always paged, so file size stops mattering. The hex view is always
one page (default 128 KiB) with absolute file offsets — a small file
simply has exactly one.
:HexPairOpenopens a file of any size instantly, reading only the page it shows and never loading the rest.:wwrites the page back: overwriting values patches it in place, so the rest of the file is never read or written whatever its size; inserting bytes moves only what follows them, also in place; and only deleting them rewrites the file — except past 2 GiB on native Windows, where that is in place too. Either change of length says what it will cost and asks first. See Pages.
Vim 8.0 or later, and xxd — which ships with Vim. Nothing else, and no
Neovim: this is a Vim plugin, tested against Vim 8.0.0000, current Vim and
Vim on Windows, and it uses readblob() and Vim9 script where they are
there (What it costs).
Vim 8+ native packages (recommended):
mkdir -p ~/.vim/pack/plugins/start
# Check the signature before this line - see "Verifying Releases" below.
tar xf hexpair.vX.Y.Z.tar -C ~/.vim/pack/plugins/start/
vim -c 'helptags ALL' -c 'q'Which file to take. Every release carries two, and either one installs the same working plugin:
hexpair.vX.Y.Z.tar |
The complete release. Take this one. |
hexpair.vX.Y.Z.minimal.tar.bz2 |
The same plugin, compressed, without CLAUDE.md, CONTRIBUTING.md and CHANGELOG.md. |
The smaller one exists because vim.org refuses an upload somewhere between 224 and 250 KiB, and the complete tarball is 900 KiB. Nothing the plugin needs to run is missing from it: the plugin itself, the help, the mappings, the shell and Windows commands, the registry files for the Explorer menu, the icons, the licence and the README are all there. What is gone is reading matter that is a click away here — the project's own notes, the contributor guide, and the changelog, which on vim.org is in that site's own per-version release notes anyway. Both archives are signed, and unpack the same way:
tar xf hexpair.vX.Y.Z.minimal.tar.bz2 -C ~/.vim/pack/plugins/start/Each release archive is accompanied by a detached GPG signature file —
hexpair.vX.Y.Z.tar.asc for the complete tarball and
hexpair.vX.Y.Z.minimal.tar.bz2.asc for the smaller one. Before
installing, verify that the archive has not been tampered with:
gpg --keyserver keys.openpgp.org --recv-keys 489C5EC80FD62BE89E59B4F719C13E8CE0F5DB61
gpg --verify hexpair.vX.Y.Z.tar.asc hexpair.vX.Y.Z.tar
Both archives are built from the same sources by the same script and are byte-identical whichever platform builds them — CI compares the Linux and the Windows build of each on every push. The copy on vim.org is that same minimal archive; the signature for it lives here, since vim.org hosts its own copy and nothing there vouches for it.
Clone the repository directly:
git clone https://github.com/michal-ruzicka/hexpair.git \
~/.vim/pack/plugins/start/hexpair
vim -c 'helptags ALL' -c 'q'Or with a plugin manager — the repository is a plain Vim package, so every manager takes it as it comes and builds the help tags itself:
" vim-plug
Plug 'michal-ruzicka/hexpair'" minpac
call minpac#add('michal-ruzicka/hexpair')The plugin defines no key mappings by default — it provides commands
and <Plug> targets, and which keys those go on is yours to decide.
A ready-made set comes with it, in hexpair.vimrc. One line in your vimrc,
after mapleader is set, and you have all of it:
runtime pack/*/start/hexpair/hexpair.vimrcThat works on Linux, Windows and WSL alike and needs no path of yours in
it: 'runtimepath' already points at the per-user Vim directory of the
platform — ~/.vim on Unix, ~/vimfiles on Windows — and :runtime
searches it. (An absolute source ~/.vim/pack/… would be wrong on
Windows for exactly that reason, even though Vim does expand ~ there.
And package directories are not on 'runtimepath' yet while a vimrc
runs, which is why the path above says where to look; with a plugin
manager, whose directories are on it by then, plain runtime hexpair.vimrc is enough.)
The file never takes a key you have already mapped, so your own mappings win: define them before that line. What it sets up, and what to copy if you would rather pick your own keys:
" Views
nmap <Leader>h <Plug>(HexPairToggle) " hex page view <-> windowed text view
nmap <Leader>u <Plug>(HexPairUnhex) " back to the plain, unpaged buffer (a toggled file only)
nmap <Leader>< <Plug>(HexPairGoHex) " cursor to the HEX column, same byte
nmap <Leader>> <Plug>(HexPairGoAscii) " cursor to the ASCII column, same byte
nmap <Leader>- <Plug>(HexPairSwap) " cursor to the opposite column
nmap <Leader>r <Plug>(HexPairRefresh) " regenerate offsets/ASCII, no write
" Moving around the file
nmap <Leader>j <Plug>(HexPairPageNext) " next page
nmap <Leader>k <Plug>(HexPairPagePrev) " previous page
nmap <Leader>g <Plug>(HexPairPageGoto) " prompt for a page (N, +N, -N, $, $-N)
nmap <Leader>b <Plug>(HexPairGoOffset) " prompt for a byte, 1-based (0x..., +N, -N, $, $-N)
nmap <Leader>? <Plug>(HexPairPages) " where am I: page, range, cursor byte
nmap <Leader>= <Plug>(HexPairSyncViews) " bring the other bound view to my byte
" Reading the bytes
nmap <Leader>i <Plug>(HexPairInspect) " the bytes at the cursor as numbers
nmap <Leader>I <Plug>(HexPairInsertChar) " prompt for a character, write its bytes
nmap <Leader>s <Plug>(HexPairSelection) " how many bytes the last selection was
xmap <Leader>s <Plug>(HexPairSelection) " ... and the one being made now (kept)
" Marks - all four under one prefix, because single letters run out
nmap <Leader>ml <Plug>(HexPairMarks) " list the marks in this file
nmap <Leader>ms <Plug>(HexPairMark) " prompt for a name, mark this byte
nmap <Leader>md <Plug>(HexPairMarkDelete) " prompt for a mark to drop
nmap <Leader>mg <Plug>(HexPairGoMark) " prompt for a mark to go to
nmap <Leader>mG <Plug>(HexPairGoMarkForce) " ... discarding unwritten changes
" Searching, and comparing with another file
nmap <Leader>/ <Plug>(HexPairFind) " prompt for bytes to find
nmap <Leader>t <Plug>(HexPairFindText) " prompt for text to find
nmap <Leader>f <Plug>(HexPairFindNext) " next match of the last pattern
nmap <Leader>F <Plug>(HexPairFindPrev) " previous match
nmap <Leader>e <Plug>(HexPairModifiedNext) " next run of bytes you edited
nmap <Leader>E <Plug>(HexPairModifiedPrev) " previous one
nmap <Leader>M <Plug>(HexPairModified) " stop marking them, or start again
nmap <Leader>d <Plug>(HexPairModifiedShow) " what was here before you edited it
xmap <Leader>d <Plug>(HexPairModifiedShow) " ... for a whole selection
nmap <Leader>] <Plug>(HexPairDiffNext) " next change against that file
nmap <Leader>[ <Plug>(HexPairDiffPrev) " previous one
nmap <Leader>D <Plug>(HexPairDiffShow) " what that file has here
xmap <Leader>D <Plug>(HexPairDiffShow) " ... for a whole selection
nmap <Leader>c <Plug>(HexPairFindClear) " stop marking the matches
nmap <Leader>C <Plug>(HexPairDiffClear) " stop comparing, clear the marking
" Uppercase variants: the same, but discard unwritten changes without
" asking (like the ! commands) - handy for skimming through a file.
nnoremap <silent> <Leader>J :HexPairPageNext!<CR>
nnoremap <silent> <Leader>K :HexPairPagePrev!<CR>
nnoremap <silent> <Leader>U :HexPairUnhex!<CR>
nmap <Leader>G <Plug>(HexPairPageGotoForce)
nmap <Leader>B <Plug>(HexPairGoOffsetForce)Note the xmap in the third block: it is the same <Plug> target in
Visual mode, where it reports the selection you are making rather
than the last one — and puts the selection back afterwards, since asking
about it from the : line is what ends Visual mode. Every other target is Normal-mode only. The commands
that take an argument — :HexPairFind, :HexPairReplace,
:HexPairDiff, :HexPairSplit, :HexPairOpen — have no <Plug>
target, so there is nothing to map for them. The four mark commands do:
the two that need a name (:HexPairMark, :HexPairMarkDelete, and
:HexPairGoMark beside them) ask for it and complete the names that
exist, the way <Plug>(HexPairPageGoto) asks for a page.
:HexPairOpen {file} takes an argument, so it has no <Plug> target;
it is meant for the command line or a shell wrapper (see below). For a
name built programmatically there are function forms —
HexPairOpenFile({file} [, {page}]) and HexPairDiffWith({file}) —
because a name containing a space or a literal $ does not survive the
Ex command line's own argument parsing.
<Leader> expands to the mapleader variable at the time a mapping is
defined — backslash by default; put e.g. let mapleader = ',' before
the mappings to use a different prefix. The <Plug>(HexPair…) targets
are named virtual keys exposed by the plugin — map onto them with
nmap, not nnoremap (the latter forbids the remapping through which
a <Plug> target expands). Details: :help hexpair-mappings.
Open a file, preferably in binary mode:
vim -b file.binand toggle the hex view with your mapping or :HexPairToggle. For a file
too large to want Vim to read at all, skip the buffer entirely:
vim -c 'HexPairOpen /var/lib/disk.img'A shell wrapper for the same ships with the plugin. Source it from your
~/.bashrc:
source ~/.vim/pack/plugins/start/hexpair/hexpair.bashrcand it gives you vimhex:
vimhex bigfile.bin # the first page
vimhex bigfile.bin 3 # page 3
vimhex bigfile.bin '$' # the last page, without counting them
vimhex bigfile.bin '$-5' # five pages back from the end
vimhex bigfile.bin '@$-0x100' # 0x100 bytes back from the last one
vimhex bigfile.bin @0x4a2000 # the page holding that byte
cat bigfile.bin | vimhex - # piped input:HexPairPages reports the byte under the cursor in exactly the form
@BYTE takes, so you can note where you were and come straight back to
it later. Set VIMHEX_VIM to pick a particular Vim.
The same file defines gvimhex and gvimhexdiff: those two commands
again, taking the same arguments, with VIMHEX_VIM defaulting to gvim
instead of vim.
gvimhex bigfile.bin
gvimhexdiff old.img new.imgA VIMHEX_VIM you have already set is left alone, so it still names the
Vim when you have pointed it at a particular gVim. All four are one
argument grammar — the g pair delegates rather than repeating it — and
cmd.exe gets the same four as vimhex.cmd, vimhexdiff.cmd,
gvimhex.cmd and gvimhexdiff.cmd (see
vimhex and vimhexdiff on Windows).
Details are in the file's own comments and in :help hexpair-vimhex.
A buffer hexpair has touched is in one of two views, and :HexPairToggle
moves between them:
| View | What you see |
|---|---|
| Hex page | the page as an xxd dump — offset, hex and ASCII columns — between two banner lines |
| Windowed text | the same page's raw bytes as text, between the same banner lines |
For a file you opened normally (vim file.md) and then toggled to hex with
:HexPairToggle, :HexPairUnhex is the way back: it re-opens the whole
file as the ordinary, unpaged, non-binary view it was before, with the
'binary', 'fileencoding', 'fileformat' and the other options it had
then, and the cursor where it was. The buffer is re-read, not converted,
because a page is not the file and dressing it up as one would be a lie.
A file opened as hex (:HexPairOpen, vimhex) has no plain buffer to
go back to — it may have had no text view at all, and its file may be far
too large to load for the purpose — so :HexPairUnhex refuses it and says
so, naming the file to :edit instead. A paged view that was never backed
by a file (e.g. piped into Vim) is refused for the same reason. In neither
case would a plain :w help: it would truncate the file down to that one
page.
| Command | Description |
|---|---|
:HexPairToggle |
Move between the hex page view and the windowed text view |
:HexPairUnhex[!] |
A buffer that toggled from a plain buffer: back to the ordinary, unpaged, non-binary view, re-opened with the options it had before; ! discards unwritten edits to the page. A buffer opened as hex has no plain view to return to, and this says so |
:HexPairGoHex / :HexPairGoAscii / :HexPairSwap |
Move the cursor between the HEX and ASCII columns, staying on the same byte |
:HexPairRefresh |
Regenerate the offset and ASCII columns from the current hex payload, without writing |
:HexPairOpen {file} [page] |
Open {file} paged, without loading it; [page] takes +N/-N, $ and $-N too |
:HexPairPageNext[!] / :HexPairPagePrev[!] / :HexPairPageGoto[!] {page} |
Turn pages (! discards unwritten changes); {page} is a number, +N/-N to step, $ for the last one, or $-N for N back from it |
:HexPairGoOffset[!] {byte} |
Jump to a byte, decimal or 0x-prefixed, turning the page if needed; 1-based, like the banner. +N / -N step from where the cursor is, $ is the last byte and $-N is N back from it ($-0x10 too) |
:HexPairPages |
Report page X of Y, the offsets covered, the file size and the byte under the cursor |
:HexPairSyncViews |
Bring every scroll-bound view onto the byte this one is on |
:HexPairInspect[!] |
Read the bytes at the cursor as numbers: 8/16/32/64-bit, unsigned and signed, both endiannesses, and both IEEE 754 floats — and mark them on the page; ! unmarks them |
:HexPairInsertChar [++enc={encoding}] {text} |
Put the bytes of {text} in at the cursor, in g:hexpair_insert_encoding or in {encoding} |
:HexPairSelection |
Say how many bytes the Visual selection covers, and which |
:HexPairFind[!] {bytes} |
Find those bytes in the file (? = any nibble); ! forgets the pattern |
:HexPairFindText {string} |
The same, for the bytes of a string |
:HexPairFindNext / :HexPairFindPrev |
Repeat the search either way (obeys 'wrapscan') |
:HexPairReplace {bytes} |
Put those bytes over the match under the cursor |
:HexPairReplaceAllInPage {pattern} / {bytes} |
... over every match on the page in view |
:HexPairModifiedNext / :HexPairModifiedPrev |
Walk the runs of bytes edited and not yet written, on this page |
:HexPairModifiedShow |
What was here before you edited it, at the cursor or over a Visual selection |
:HexPairModified[!] |
Stop marking edited bytes, or start again; ! stops rather than toggling |
:HexPairDiff[!] [file] |
Compare with {file}, marking the bytes that differ; ! stops |
:HexPairDiffNext / :HexPairDiffPrev |
Walk to the next/previous byte where the two files differ |
:HexPairDiffShow |
What the compared file holds at the cursor, or over a Visual selection — including that it has nothing there |
:HexPairMark {name} / :HexPairGoMark[!] {name} / :HexPairMarks / :HexPairMarkDelete {name} |
Remember a byte of the file, jump back to it, list them, drop one |
:HexPairSplit [page] / :HexPairVSplit [page] |
A second view of the same file in a new window, showing [page] (default: this view's) |
Every command answers to a short name as well — :HPFind,
:HPReplaceAllInPage, :HPToggle — because :HexPair… is a lot to type
at a : prompt. Same arguments, same bang, same completion;
g:hexpair_short_commands = 0 leaves that namespace alone.
:HexPairInspect (mapped to <Leader>i above) reads the eight bytes at
the cursor as everything they could be:
hexpair: byte 66 (0x42) of 512: 41 42 43 44 45 46 47 48
8-bit 65 char 'A' bin 01000001 oct 0101
little-endian big-endian
16-bit 16961 16706
32-bit 1145258561 1094861636
64-bit 5208208757389214273 4702394921427289928
float32 781.035217 12.141422
float64 1.58398e40 2393736.541207
utf-8 U+0041 'A' (1 byte)
utf-16 U+4241 '䉁' (2 bytes) U+4142 '䅂' (2 bytes)
utf-32 U+44434241 - past U+10FFFF U+41424344 - past U+10FFFF
block Basic Latin
The last rows say what the character is. They describe the UTF-8
reading, which is the one with no byte order to choose. name appears for
a code point you cannot see — every C0 and C1 control, DEL, and the space
and format characters a hex editor meets constantly; block is the Unicode
block, which answers what script even is this; and bom appears when the
bytes at the cursor are a byte order mark, which is the one thing the UTF-8
rows cannot say, since ff fe is not UTF-8 at all. The byte order mark is
the one named character that gets no block row: U+FEFF is assigned to
Arabic Presentation Forms-B only because Unicode lays out blocks by
contiguity and FE70..FEFF is the range that happens to close the BMP. It
is a format character that belongs to no script, so a block row would only
add a script where there is none — the name row already says what it is.
So an ESC at the start of a UTF-16LE file ends:
utf-8 U+001B (1 byte)
name ESC (escape), a C0 control
bom utf-16le byte order mark
There is deliberately no full character name — the LATIN SMALL LETTER E WITH ACUTE kind. That needs UnicodeData.txt, two megabytes of names for
a hundred and fifty thousand characters, which is a different order of
thing from a table of ranges and does not belong inside a Vim plugin. The
block table is Unicode 16.0.0, generated by make-unicode-blocks.py from
the file it pins by digest — by hand, when the Unicode version is bumped;
nothing is generated at package time or on your machine. It is derived from
the Unicode Character Database under the Unicode License V3, whose notice
NOTICE.md carries in full.
It works outside a hex view too. <Leader>i in an ordinary buffer of
ordinary text answers the same questions — what is this character, is that
a NBSP, does this file start with a BOM — with two differences it tells you
about. The bytes are Vim's, not the file's: a paged view holds what is
on disk because hexpair opened it ++bin, while an ordinary buffer holds
what Vim read it into, so a file read as latin1 into a UTF-8 Vim is latin1
on disk and UTF-8 in memory. A note row says so whenever the two can
differ:
note Vim's bytes, not the file's: 'fileencoding' is latin1, this Vim holds utf-8
note a line break reads 0a here; 'fileformat' is dos, so the file has 0d 0a
And nothing is marked on screen there, since the marking belongs to the highlighting a paged buffer has and an ordinary one does not.
The bytes it read are marked on the page as well (HexPairInspect, which
follows Visual by default), in both columns, for as long as the cursor
stays on the byte they were read from. Near the end of a page or of the
file there are fewer than eight of them, and the marking is short with the
report rather than claiming eight. :HexPairInspect! takes the marking
off at once.
:HexPairInsertChar (mapped to <Leader>I above, which asks for the
text) is the inspector read backwards. The inspector says what the bytes
at the cursor would be as utf-8, utf-16 and utf-32; this writes a
character in exactly those:
:HexPairInsertChar Š " c5 a0 - g:hexpair_insert_encoding
:HexPairInsertChar ++enc=utf-16le Š " 60 01 - just this once
:HexPairInsertChar ++enc=utf-32be Š " 00 00 01 60
:HexPairInsertChar ++enc=cp1250 Š " 8a - if this Vim's iconv knows itThe bytes go in before the byte under the cursor and push the rest of
the page along — the same edit as typing them into the dump, so one u
takes the whole insert back, nothing reaches the file until :w does, and
the write that carries it is the length-changing one that says what it
will cost and asks.
Which encoding is meant is g:hexpair_insert_encoding ('utf-8' unless
you say otherwise): a file is in one encoding, so the question is worth
answering once. ++enc= overrules it for a single insert without changing
it back afterwards.
utf-8, utf-16le/be, utf-32le/be, latin1 and ascii are computed from
the character's code point, so they do not depend on what the machine's
iconv was built with — and, more to the point, they can contain a NUL.
A in utf-16le is 41 00, and a converted answer would end at the first
of those, because a Vim string cannot hold a NUL. Any other encoding name
is handed to iconv(), and then converted back and compared: a
conversion that lost a byte does not survive the round trip, and a name
this Vim does not know is refused rather than quietly written as utf-8.
The signed reading follows the unsigned one where the two differ
(43981 / -21555). The three text rows say what the bytes would be as
characters — UTF-8 has no byte order to get wrong, the other two are read
each way round like the numbers above them — and every way an encoding
can be wrong is its own answer rather than a code point: an overlong
UTF-8 sequence, a lone surrogate or a value past U+10FFFF is not a
character, and saying so is the point. The bytes are this page's, as the buffer holds them
— edits included — so at the end of a page the wider rows say how many
are left instead of reaching into a page that is not on screen.
:HexPairSelection (<Leader>s, in Visual mode too) says what a
selection covers:
hexpair: 18 bytes selected, 1041-1058 (0x411-0x422) of 5000
hexpair: 12 bytes selected in 3 lines (4 per line), 1041-1076 (0x411-0x434) of 5000
The second form is a blockwise selection, whose bytes are not one run —
so it leads with the count. Both work in either view, and the numbers
are 1-based, the same as the banner's, so they can be typed straight
into :HexPairGoOffset.
Asked from Visual mode it puts the selection back and then waits for a
key, because Vim draws its own -- VISUAL -- over the message line as
soon as it is back in Visual mode — a report that did not wait would be
gone before it could be read. Press Enter and the selection is still
there. From Normal mode nothing redraws over it, so nothing waits.
Editing rules (see :help hexpair for details): keep bytes in the hex
area separated by at most one space — a run of two spaces marks the
start of the (ignored) ASCII column — and always write full byte pairs.
The banner lines are decoration and contribute no bytes; in the windowed
text view they are matched by their exact text, so editing one refuses
the write rather than guessing which lines are content.
:w writes just the page you are looking at. If you only overwrote
values, it is patched in place — the file keeps its length, every
byte outside the page keeps its content, and the cost does not depend on
the file's size. If you inserted bytes, only what follows them has to
move, and it moves in place too — what precedes them is not even read.
Only deleting bytes rewrites the file, because nothing in Vim or xxd
can make a file shorter any other way. Either change of length says how
the size will change and how much has to be written, and asks first.
Where the pages come from depends on what the buffer was:
- unmodified and backed by a file — the usual case: pages are read from the file;
- no file at all (
cat data | vim -) — the content is written once to a private temporary file and paged from there. A plain:whas nothing to write back to;:w {file}saves all of it and the view then edits that file. Pipe binary data in withvim -b -, or Vim may transcode it on the way in — hexpair warns when it does; - modified and backed by a file — refused, because the buffer and
the file disagree and every way of resolving that loses something
quietly. Write it first, or use
:HexPairOpento see what is on disk.
/ searches the page on screen — which is a window on the file, so it
cannot find what is on any other page, and a sequence of bytes in a dump
has spaces, line breaks and an ASCII column through the middle of it.
:HexPairFind searches the file:
:HexPairFind de ad be ef " or deadbeef, or de ?? be ef
:HexPairFindText Makefile " the bytes of a string, taken as typed
:HexPairFindText Škola " ... in whatever 'encoding' spells it as
:HexPairFindNext " and again, obeying 'wrapscan'Every match on the page is marked (HexPairFind), and the search lands
the cursor on the byte it found, turning the page if it is elsewhere.
What is searched is the file, with the page in view as you have it:
bytes typed over the dump and not yet written are found where they are
shown, and the bytes they replaced are not found any more. Only one page
can be in that state, since turning a page needs an unmodified buffer, so
the rest of the file is the file. Comparing (:HexPairDiffNext) reads
this side the same way; the file it is compared against is read as it
is on disk. Details, including what an insert defers until :w:
:help :HexPairFind.
:HexPairReplace 11 22 33 44 " over the match under the cursor
:HexPairReplaceAllInPage de ad be ef / 00 00 " over every match on this pageBoth edit the page exactly as typing over the dump would: the new bytes
are marked as changed, and nothing reaches the file until :w does — so
a replacement of a different length asks the same question any other
insertion or deletion asks.
The second command has the scope in its name because the scope is the surprising part: everything here writes one page at a time, and a file-wide replace would be a different mechanism with a different failure to recover from, not a bigger version of this one. Step the pages to cover a file.
What you have changed and not yet written is marked as you type it, and
:HexPairModifiedShow says what the bytes it marks used to be:
:HexPairModifiedShow " the byte under the cursor, or a selectionhexpair: bytes 17-18 (0x11-0x12), 2 of 2 edited
here ff ee
disk 10 11
The marking covers the new byte, so the old one is exactly what the
screen no longer shows — and bytes an insert has added have nothing
behind them at all, which comes out as -- rather than as a plausible
00. It is the :HexPairDiffShow below, asked of this view's own file
instead of another one and down to the two rows, so <Leader>d is the
lowercase of that one's <Leader>D.
Comparing with another file works the same way round:
:HexPairDiff ../golden/firmware.bin " mark what differs on this page
:HexPairDiffNext " the next change, wherever it is
:HexPairDiff! " stop comparing, and clear the marking:HexPairDiffNext / :HexPairDiffPrev move between changes, not
through the bytes of one: a run of differing bytes is one change however
long it is. Where two files agree on byte 1, differ over bytes 2–5, agree
again over 6–8 and differ from byte 9 on, that is two changes — byte 2
and byte 9 — and a third press says there is nothing after them.
Backwards works the same way and lands on a change's first byte, so from
the middle of one it goes to that change's own start, as [c does in a
diff.
And the shell wrapper opens two files that way in one go:
vimhexdiff old.img new.img— both files side by side, each marking what differs from the other, cursors on the first difference and the windows scroll-bound.
Searching and comparing read the file a block at a time
(g:hexpair_scan_block, 8 MiB by default), so neither has to fit in
memory. On a large one that takes long enough to look like a hang, so
from 16 MB up the scan says how far it has got, and CTRL-C stops it —
nothing has been changed by then, both only read.
The byte markings — what differs, what matches a search, what you have edited, where a mark is — are drawn in both views, over the lines on screen: three columns per byte in the dump, one per byte in the windowed text view. The line break that ends a text-view line is a byte of the page with no column of its own, so it is the one byte never marked; and because Vim cannot hold a NUL in a string and writes one as a line break, a NUL replaced by a line break at the same offset is the one edit the text view does not mark.
Everything is measured in bytes, so CRLF line endings and UTF-8 text mark
exactly what is meant: a CR is a byte of the page like any other, and one
byte of a multi-byte character is marked as one byte. The only thing a
byte offset cannot do in the text view is put the cursor inside a
character — :HexPairGoOffset on the second byte of a two-byte character
lands on the character and says which offset it reached. The hex view
reaches every byte. And what is marked is the byte, while what you
see is the character it belongs to — a character is one cell on screen,
and Vim colours cells; the hex view shows which byte it was.
'scrollbind' says the two windows move together, and a page turn is the
one kind of scrolling Vim cannot follow on its own — so hexpair passes it
on: turning the page in either window (or landing on another page with
:HexPairDiffNext, :HexPairFind, a mark) takes every scroll-bound
window with it, to the page holding the same byte, cursor included.
A window with unwritten changes is left where it is and says so, rather
than having them discarded on its behalf. A window whose own file does
not reach that far goes to the page anyway and says the page is not
there — a banner, no bytes, and a :w refused — because two windows
showing different offsets side by side, with nothing saying so, is what
binding them is meant to prevent. g:hexpair_bind_pages = 0 turns this
off and lets 'scrollbind' mean scrolling alone.
Vim's own marks are positions in a buffer, and a paged buffer holds a different part of the file from one page to the next. These are positions in the file:
:HexPairMark header " remember the byte under the cursor
:HexPairGoMark header " go back to it, wherever it is
:HexPairMarks " list them, by position, with the page each is on
:HexPairMarkDelete headerThey are kept per file, so two views of one file share them, and they
last as long as the Vim session does. A mark on the page in view
underlines the byte it stands on, in both columns — underline rather
than a colour, because a mark says this place while the three
colourings around it (edited, differing, found) say these bytes, and
where they land on the same byte the colouring wins. g:hexpair_show_marks
turns the underlining off.
:HexPairSplit opens a second window onto the same file at another page,
and :HexPairVSplit does it vertically:
:HexPairSplit +1 " the next page beside this one
:HexPairVSplit $ " the end of the file next to where you are
:HexPairSplit 7 " page 7
:HexPairSplit " the same page again, to navigate away from[page] is counted from the view you are in, exactly as
:HexPairPageGoto counts it. The two views share nothing but the file:
each is its own buffer with its own page, cursor and unwritten changes,
and a :w in either patches only the page that view holds — so one
region can be read while another is edited, or bytes copied from one to
the other. The second buffer is named disk.img [hexpair page #2], which
is what tells them apart in :ls.
Writing one view does not lock the other out: a write asks whether its own page changed on disk, not whether the file did. Two views of the same page are allowed too — and then the second write is refused, because that page really did change underneath it.
A plain :split on a hex page is left alone: it does what :split means
everywhere else in Vim — two windows onto one buffer, so both show
the same page and turning it in one turns it in the other. That is worth
keeping, because a page is thousands of lines and looking at two parts of
one page is a real use of a split. If you would rather every split be an
independent view, say so:
let g:hexpair_split_views = 1and then :split, :vsplit, :tab split — any way a page ends up in a
second window — give you a view of its own, on the same page and the same
byte, to navigate away from. :HexPairSplit does it explicitly either
way.
A view paged from piped input cannot be split: the temporary file it
pages belongs to that buffer alone. Save it with :w {file} first. (With
g:hexpair_split_views on, a :split of such a view simply stays an
ordinary split rather than complaining.)
Every option, with its default, ready to paste into ~/.vimrc — the
values shown are the defaults, so uncomment a line only to change one.
" ---- hexpair -------------------------------------------------------------
" Bytes per dump line.
" let g:hexpair_bytes_per_line = 16
" Bytes per page. Must stay a positive multiple of
" g:hexpair_bytes_per_line. A page is an ordinary Vim buffer, so a bigger
" one costs what that many lines cost - see "What it costs" below.
" let g:hexpair_page_size = 128 * 1024
" Whether a write that changes the page's length says what it will cost
" and asks first. Set it to 0 to answer yes automatically, e.g. in a script.
" let g:hexpair_page_confirm = 1
" How much of the FILE a file-wide scan reads at a time - :HexPairFind and
" the comparison behind :HexPairDiffNext. Nothing to do with a page: this
" is what a scan costs in memory, and it is the same for any size of file.
" Between 1 MiB and 1 GiB; bigger buys fewer xxd processes and little else
" past 8 MiB - see "What it costs" below.
" let g:hexpair_scan_block = 8 * 1024 * 1024
" Keep the global 'paste' option on while the cursor is in a hex buffer,
" and restore it when the cursor leaves. 0 leaves 'paste' alone.
" let g:hexpair_paste = 1
" A ruler line under the banner, numbering the byte columns of the dump.
" Set it to 1 to get one.
" let g:hexpair_ruler = 0
" Whether a plain :split (or :vsplit, or :tab split) of a hex page becomes
" an independent view of the same file - its own page, its own cursor -
" instead of a second window onto the same buffer. Set it to 1 to get
" views; :HexPairSplit does the same explicitly whatever this says.
" let g:hexpair_split_views = 0
" Whether a write made through PowerShell reads back what it wrote before
" reporting success. Past 2 GiB on native Windows only - everywhere else
" the writer is xxd and nothing reads anything back.
" let g:hexpair_verify_writes = 1
" Position-mapping trace for diagnosing a cursor that landed on the wrong
" byte. Set it to 1 and read the trace with :messages.
" let g:hexpair_debug = 0
" Whether the bytes that differ from the ones on disk - what you have
" edited and not yet written - are highlighted. Set it to 0 to stop.
" let g:hexpair_show_modified = 1
" Whether the byte a mark stands on is underlined on the page.
" let g:hexpair_show_marks = 1
" Whether the bytes :HexPairInspect has just read are marked on the page,
" for as long as the cursor stays on the byte they were read from.
" let g:hexpair_show_inspect = 1
" Which encoding :HexPairInsertChar writes a character in. utf-8,
" utf-16le/be, utf-32le/be, latin1 and ascii are computed exactly;
" anything else is left to this Vim's iconv, and refused if it has none.
" let g:hexpair_insert_encoding = 'utf-8'
" Whether every command is also defined under a short "HP" name -
" :HPFind for :HexPairFind, and so on.
" let g:hexpair_short_commands = 1
" Whether a page turn is passed on to the windows scroll-bound to this one
" (what `vimhexdiff` sets up). Set it to 0 to have 'scrollbind' mean
" scrolling alone, and let each window keep its own page.
" let g:hexpair_bind_pages = 1
" Highlight overrides: the byte under the cursor, its counterpart in the
" other column, the banner (and ruler) lines, the bytes changed since the
" page was read, the bytes that differ from the file being compared
" against, and the matches of the last search. The values below are the
" defaults - links, so that the markings look like whatever your colour
" scheme does with a diff or a search. An override should set a foreground
" AND a background, since giving only one leaves the other at the colour
" scheme's and the two can land on top of each other.
" highlight HexPairActive cterm=underline gui=underline
" highlight link HexPairMirror IncSearch
" highlight link HexPairPageBanner Comment
" highlight link HexPairModified DiffChange
" highlight link HexPairDiff DiffAdd
" highlight link HexPairFind Search
" highlight HexPairMark term=underline,bold cterm=underline,bold gui=underline,bold
" highlight link HexPairInspect Visual
" The page and the byte under the cursor, in the statusline. Empty in
" every buffer hexpair has not touched, so one statusline serves both.
" set statusline=%f\ %h%w%m%r\ %{HexPairStatus()}%=%l,%c%V\ %PWith the ruler on, a page looks like this — the ruler is decoration, it carries no bytes and is never written:
" hexpair: page 2/10 bytes 513-1024 of 5000 disk.img
" 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 0123456789abcdef
00000200: 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ................
HexPairStatus() gives hex 3/349 @0x50a01 (330241) in the hex view
and txt 3/349 @0x50a01 (330241) in the text view — the byte in both
bases, hex as the dump's own offset column speaks it and decimal as
everything else does. A page with unwritten edits is marked
hex 3/349+ @…, and the byte is then where the layout puts it — use
:HexPairPages for the counted answer once you have inserted or deleted
digits. A cursor standing in the offset column is on that line's first
byte, which is what the column says.
The plugin also bundles a filetype plugin (ftplugin/xxd.vim) with
editing defaults for the dump: tabstop=10, expandtab,
shiftwidth=3 (one hex byte) and no automatic formatting, all reverted
when hex mode is toggled off. To suppress it, put
let b:did_ftplugin = 1 into your own ~/.vim/ftplugin/xxd.vim; to
tweak individual settings, use ~/.vim/after/ftplugin/xxd.vim (see
:help hexpair-ftplugin).
Full documentation: :help hexpair after installation, or
doc/hexpair.txt.
vimhex.cmd and vimhexdiff.cmd are the same two commands for cmd.exe,
taking the same arguments as the shell functions above:
vimhex bigfile.bin REM the first page
vimhex bigfile.bin 3 REM page 3
vimhex bigfile.bin $ REM the last page, without counting them
vimhex bigfile.bin $-5 REM five pages back from the end
vimhex bigfile.bin @$-0x100 REM 0x100 bytes back from the last one
vimhex bigfile.bin @0x4a2000 REM the page holding that byte
type bigfile.bin | vimhex - REM piped input
vimhexdiff old.img new.img REM the two side by sideThey default to the console vim. Set VIMHEX_VIM for another one —
gvim for the GUI, or a full path such as
C:\Program Files\Vim\vim91\gvim.exe.
gvimhex.cmd and gvimhexdiff.cmd are the same two commands again
(gvimhex/gvimhexdiff in hexpair.bashrc are the Bash pair),
defaulting to gvim instead — for double-clicking a file, or wiring into
the Explorer context menu below, where there is no console for vim to run
in and no way to pass VIMHEX_VIM in anyway. They delegate to
vimhex.cmd/vimhexdiff.cmd rather than duplicating their argument
parsing, so keep all four files together; a VIMHEX_VIM already set in
your environment overrides gvim there too.
Where to put them. They only need to be on PATH, and the plugin's own
directory is the tidiest place to point at, because then updating the plugin
updates the commands:
%USERPROFILE%\vimfiles\pack\plugins\start\hexpair
Add it through Settings → System → About → Advanced system settings →
Environment Variables (or rundll32 sysdm.cpl,EditEnvironmentVariables),
editing Path under User variables. Do not do it with
setx PATH "%PATH%;...": %PATH% there is the system and user paths already
joined together, so that writes the whole lot into your user Path and
truncates it at 1024 characters.
Copying the four files into a directory you already have on PATH works
just as well — they find Vim through PATH or VIMHEX_VIM rather than
through where they sit themselves. gvimhex.cmd/gvimhexdiff.cmd do
depend on where vimhex.cmd/vimhexdiff.cmd sit, though: copy all four
together, not a pair on their own.
Two ready-made files ship with the plugin: vimhex-contex-entry.add.reg
adds one vimhex submenu in a single import, and
vimhex-contex-entry.remove.reg takes it out again. For a default install
there is nothing to edit — double-click either, or:
reg import "%USERPROFILE%\vimfiles\pack\plugins\start\hexpair\vimhex-contex-entry.add.reg"The paths inside are written against
%USERPROFILE%\vimfiles\pack\plugins\start\hexpair, the package directory
the install instructions above use. What you get:
vimhex ▸ gvimhex this the file you right-clicked, in hex
──────────────────────────
gvimhexdiff select as left this is the left-hand side
gvimhexdiff select as right this is the right-hand side
The two diff entries are symmetric — pick either side first. Each one
records its side and stops; whichever completes the pair opens the
comparison and clears both, so the next diff starts clean. Picking the same
side twice just overwrites it, and there is no order to get wrong. The same
two steps work from cmd.exe:
vimhexdiff /left old.img
vimhexdiff /right new.imgThe diff opens maximized, in gVim: two hex views side by side want the full width.
Everything lives under HKEY_CURRENT_USER, so it needs no administrator
rights and touches nobody else's account. On Windows 11 it is a "legacy"
context menu and therefore sits under Show more options (Shift+F10 opens
that directly); file managers that use the classic menu — Total Commander
among them — show it straight away. A console window flashes while the
.cmd runs, which is accepted rather than worked around; CONTRIBUTING.md
says why, along with how the two files are built and why they are generated
rather than edited.
Installed somewhere else? Re-generate the pair rather than editing them:
python3 make-context-entry-reg.py "D:\your\path\hexpair"Files over 2 GiB work on native Windows, and nothing about using them is
different. What changes is how the bytes are reached: xxd seeks with a
C long, which is 32 bits there, and Vim's readblob() has a limit of its
own — so past that offset hexpair does the seeking with PowerShell, which
Windows ships, and everything keeps working. Reading, searching, comparing,
overwriting, growing, shrinking, :w {file}.
What it costs is one process start per page turn, a few hundred milliseconds; a file-wide scan pays it per block and is correspondingly slow. One thing gets cheaper: shrinking a file, which everywhere else means writing the whole thing out again, is a tail slide and a truncation here.
:help hexpair-windows-2gib has the rest — both places the limit lives,
what runs where, and why PowerShell is not used on any other platform.
The hex view is always one page. A file smaller than a page has exactly one; a 400 GB one has as many as it needs, and only the page on screen is ever read.
| Command | Description |
|---|---|
:HexPairOpen {file} [page] |
Open {file} paged at [page] (1-based, default 1; +N/-N, $ and $-N work here too) without loading it |
:HexPairPageNext[!] / :HexPairPagePrev[!] |
Turn the page; refuses to discard unwritten changes without ! |
:HexPairPageGoto[!] {page} |
Jump to page {page}: a number, +N / -N to step, $ for the last one, or $-N for N back from it |
:HexPairGoOffset[!] {byte} |
Jump to a byte, decimal or 0x-prefixed; 1-based, like the banner. +N / -N step from the cursor, $ is the last byte, $-N is N back from it |
:HexPairPages |
Report the current page, total pages, the byte range shown, and the byte under the cursor |
:HexPairSplit [page] / :HexPairVSplit [page] |
A second view of the same file in another window, at [page] — see above |
<Plug>(HexPairPageGoto) (mapping example above) prompts for a page
with input() instead of requiring a typed :HexPairPageGoto {page} —
press the key, type a number (or +2, -1, $, $-5), Enter.
<Plug>(HexPairPageGotoForce) is the same prompt but discards unsaved
changes without asking, like the {N} variant with !.
Each page is bracketed by a leading and trailing banner line (" hexpair: page 3/349 bytes ...), given a comment-like appearance via the
HexPairPageBanner highlight group. The banner contributes no bytes: in
the dump it is recognized by its leading double quote, and in the
windowed text view — where a page of raw bytes may itself start with one
— by matching the banner text exactly, so editing it refuses the write
rather than guessing which lines are content.
A page is an ordinary Vim buffer, so its size is what everything costs:
with 16 bytes per line, the default 128 KiB page is 8192 lines. Reading
one takes hundredths of a second, writing one about a seventh. Since
:HexPairGoOffset reaches any byte directly, raising the page size buys
nothing but latency — see :help hexpair-paged-size.
:w {file} means "save the whole thing over there", not "save this
page": it writes the entire content being paged, with the current page's
edits in it, and leaves the original alone. For piped input that is the
only way to save, and the view adopts the file afterwards.
A file that changed on disk since the page was read is refused rather
than patched blindly — and not only by its size and timestamp, which
whole-second resolution lets an in-place writer slip past: the page's
own bytes are hashed when it is read and again before it is patched.
g:hexpair_page_confirm = 0 answers the resize question automatically,
for scripts.
Only a write that shortens a file, :w {file}, and one that
inserts bytes with more than half the file after them need a newer
Vim than the rest of the plugin: +num64 and patch 9.0.0795+, for
readblob(), checked when such a write is attempted and refusing just
that write. Viewing, navigating, same-length writes and inserts with no
more than half the file behind them run on Vim 8.0 with nothing but
xxd. Details: :help hexpair-paged.
Memory does not follow the size of the file. A page is read, written and patched a block at a time, so the numbers below are the same for a file of 8 KiB and one of 8 TiB — measured with the default 128 KiB page:
| Operation | Memory | Temporary disk space |
|---|---|---|
| Viewing a page, turning pages | 12 MB | none |
| Writing a page whose length did not change | 13 MB | about one page |
| Inserting bytes | 13 MB | one block of hex, ~16 MB |
| Deleting bytes | 29 MB | a full copy of the file |
Time is what does scale, and only with what has to move: a same-length
write touches the page alone, an insert moves the bytes after it, and a
delete rewrites the file — because nothing in Vim or xxd can make a
file shorter any other way. On this machine, at the default page size,
opening or turning a page takes about 0.03 s and a same-length write
about 0.15 s; both include reading the page's bytes back to check that
nothing else has written to them meanwhile.
The one exception to all of this is :HexPairToggle on a file you have
already opened normally: by the time you press it, Vim has read the whole
file into the buffer. That is exactly what :HexPairOpen (and vimhex)
exist to avoid — they read only the page they show.
A file-wide scan — :HexPairFind, and the comparison behind
:HexPairDiffNext — is the other thing that reads more than a page, and
it reads the file in blocks of g:hexpair_scan_block (8 MiB by default,
between 1 MiB and 1 GiB). That block is the whole of what a scan costs in
memory, whatever the file's size.
Which is where the two readers come in. Where Vim can read a byte
range without xxd — readblob() with an offset and a size, which is
Vim 9.0.0795 with +num64, the same patch a splice needs — a scan works
on the raw bytes; where it cannot, it reads each block as hex through
xxd and matches text, which is what every scan used to do. The
answers are identical — the test suite holds the two against each other
and requires them to agree, and CI runs the whole suite against
Vim 8.0.0000 too. Over a 256 MiB file:
| byte reader | hex reader | |
|---|---|---|
:HexPairDiffNext to the next change |
0.28 s | 12.4 s |
:HexPairFind over the whole file |
2.0 s | 9.1 s |
| memory | 23 MB | 97 MB |
Past 2 GiB on native Windows both readers still work, and by the route
that section already describes: readblob() shares xxd's 32-bit limit
there — and answers an out-of-range read with an empty Blob and success,
which a scan would read as "nothing here" — so PowerShell writes the block
to a temp file and the bytes come out of that. The comparison and the
search are then the fast ones; the read in front of them is what dominates.
A comparison becomes two reads and a memcmp. A search has more to do —
Vim has no "find these bytes in a Blob" — so it walks every occurrence of
one byte of the pattern and checks the rest by hand, in the plugin's
one Vim9 :def function (autoload/hexpair.vim), which is five times
faster at that loop than legacy script. Which byte it walks is chosen by
sampling the block, because the walk costs a step per occurrence: looking
for 00 00 00 00 01 in 64 MiB of zeros walks the 01, finishes in
0.30 s, and would have taken 13.2 s through xxd. When every byte of
the pattern is common in a block — 00 00 in that same file — the byte
reader hands that block back and it goes through xxd, so the bad case
costs what it always did and no more.
The block a scan reads at a time is g:hexpair_scan_block, 8 MiB by
default. It barely shows on the byte reader and matters a good deal on the
hex one, where each block is an xxd process and is held at about eight
bytes of Vim per byte of block — :help g:hexpair_scan_block has the
measurements for both, and why 8 MiB is where the trade stops paying.
- Vim 8.0, any patch level — 8.0.0000 itself will do. That covers
RHEL 8 and its rebuilds, which ship
vim8.0.1763 and are supported into 2029. - Shortening a file,
:w {file}, and inserting bytes with more than half the file after them additionally need Vim 9.0.0795 with+num64— that is the patch that gavereadblob()an offset and a size. It is checked at the moment such a write is attempted, and only that write is refused, with a message saying so; everything else keeps working. See above. - The floor is not a promise, it is a build: CI compiles Vim 8.0.0000 from source on every push and runs the whole suite against it, which passes there exactly as it does on a current Vim — the checks for the three operations above are replaced by checks that they are refused and the file is left alone.
- The
xxdutility, which ships with Vim. It is looked up onPATHfirst and then in the Vim runtime directory ($VIMRUNTIME), so on Windows the bundledxxd.exeis found even when it is not onPATH.
Bug reports and patches are welcome — see CONTRIBUTING.md for the repository layout, the test harness, the reproducible release packaging and the signing policy. Project notes for AI-assisted development live in CLAUDE.md, including the architecture of the paged large-file mode.
The Unicode block table inside plugin/hexpair.vim is derived from the
Unicode Character Database and carries its own notice — see NOTICE.md,
which is bundled in every release. It does not change the terms of the
plugin.
Distributed under the same terms as Vim itself (the Vim License) — see LICENSE.md. Release notes are in CHANGELOG.md.
