Releases: michal-ruzicka/hexpair
Release list
Release v2.3.0
[v2.3.0] – 2026-09-02
Added
-
:HexPairUnhex, the way back out of a paged view. A file you opened
normally (vim file.md) and then switched to hex with<Leader>h/
:HexPairTogglecomes back to its ordinary, unpaged, non-binary view:
:HexPairUnhex(mapped to<Leader>Uinhexpair.vimrc) re-opens the
whole file with the'binary','fileencoding','fileformat'and the
other buffer options it had before the first toggle, with the cursor where
it was. The plugin takes a snapshot of those options the first time a
buffer toggles from plain, and hands them back rather than guessing. The
buffer is re-read from disk, not converted — a page is not the file, and
a page's unwritten edits would be lost, so without!the command refuses
and!discards them. What it cannot serve it refuses, each with its own
message: a file opened as hex by:HexPairOpenorvimhex, which may
have had no text view at all and whose file may be far too large to load
for one (it names the file to:editinstead); a view paged from content
with no file of its own; and one whose file has changed on disk since the
page was read. See:help :HexPairUnhex. -
:HexPairInspectnow says what the character is. Anamerow for
every code point you cannot see — the C0 and C1 controls,DEL, and the
space and format characters a hex editor meets constantly — ablockrow
naming the Unicode block, which answers what script even is this, and a
bomrow when the bytes at the cursor are a byte order mark, which the
UTF-8 rows cannot say becauseff feis not UTF-8. The byte order mark is
the one named character with noblockrow:U+FEFFsits in Arabic
Presentation Forms-B only because Unicode lays blocks out by contiguity
andFE70..FEFFcloses the BMP, and a format character belongs to no
script, so the row would only assert one where there is none. No full
character names: that needsUnicodeData.txt, which is a different order
of thing from a table of ranges. See:help hexpair-inspect-naming. -
:HexPairInspectworks in an ordinary buffer, with no hex view
anywhere near it - what is this character, is that a NBSP, does this file
start with a BOM. It says when the bytes it is showing are Vim's rather
than the file's, which is the moment'fileencoding'or'fileformat'
differs. See:help hexpair-inspect-anywhere. -
vimhex.cmdandvimhexdiff.cmd, thecmd.execounterparts of the
two shell functions inhexpair.bashrc- same names, same arguments, so
a hex view is opened the same way whichever shell you are standing in.
Put the plugin's own directory onPATHand updating the plugin updates
the commands;VIMHEX_VIMpicks the Vim,gvimincluded. -
Explorer context-menu entries, as a
vimhexsubmenu holding
gvimhex this, a separator, and gvimhexdiff select as left / select as
right. Two files take two clicks because Explorer runs a verb once per
selected file and there is no%2without a COM handler - so
vimhexdiff.cmdgrew/left FILEand/right FILE. They are
symmetric: either may be used first, each records its side and stops,
and whichever completes the pair opens the comparison and clears both
selections. Selecting the same side twice just overwrites it, and if the
other side's file has been moved or deleted since, only that selection is
cleared — the one just made is kept, so nothing has to be re-selected. -
gvimhex.cmdandgvimhexdiff.cmd, andgvimhex/gvimhexdiffin
hexpair.bashrc, opening gVim instead of console Vim by default - what a
double-click or a context-menu verb needs, since neither has a console
forvimto run in or a way to passVIMHEX_VIM. Each delegates to its
vimhex/vimhexdiffcounterpart rather than duplicating the argument
grammar. -
vimhex-contex-entry.add.regandvimhex-contex-entry.remove.reg,
ready-made versions of the registry snippets above - one import wires up
all three context-menu entries (pointing atgvimhex.cmd/
gvimhexdiff.cmd), the other removes them again. Nothing to edit for a
default install: the paths are written against
%USERPROFILE%\vimfiles\pack\plugins\start\hexpairasREG_EXPAND_SZ
values, the one registry string type whose%USERPROFILE%the shell
expands - a plainREG_SZwould have it look for a folder literally
named that. Installed elsewhere?make-context-entry-reg.pyregenerates
the pair for any path, which is also why these two files are generated
rather than hand-written:.regcan only express that type ashex(2):
plus UTF-16LE bytes. -
icons/hexpair-open.ico,-pick.ico,-with.ico, custom icons for
those three entries - Explorer has nothing to show for a verb whose
command is a.cmdfile otherwise. A V mark in Vim's own green, a0x
badge marking these as hexpair's, and on the diff pair a bigger badge of
two window panes (echoingvimhexdiff's ownvsplit) - blue left,
orange right, the side that entry represents shown at full colour and
the other dimmed, since text or an arrow stops reading reliably at
16px but colour still does. Generated byicons/build.py
(icons/design.py,icons/rasticon.py- a from-scratch PNG/ICO
encoder, no image library); only the.icofiles ship in the release
tarball, the generator is a development-only file. -
:HexPairDiffShow(<Plug>(HexPairDiffShow),<Leader>Din
hexpair.vimrc, Normal and Visual) says what the file being compared
with holds at the cursor - or over a whole selection - beside the bytes
here. The marking answers which bytes differ and stops there; this
answers what is over there instead, and in particular says when there
is nothing there because that file ends before this offset. Missing
bytes show as--, so an absent byte cannot be misread as a byte that
happens to be00. -
Files over 2 GiB on native Windows, which
xxdcannot reach at all
(see Fixed). Every operation past that offset — reading, searching,
comparing, overwriting, growing, shrinking,:w {file}— goes through
PowerShell, at the cost of one process start each. Shrinking is
actually cheaper there than anywhere else: neither Vim norxxdcan
shorten a file except by rewriting it, so a shrinking write copies the
whole file on every other platform, while.NET'sSetLengthtruncates
in place.README.md's Windows and the 2 GiB limit has the table of
what runs where. Every write past the limit reads back what it wrote
before reporting success;g:hexpair_verify_writes = 0opts out.
Changed
-
vimhexdiffopens maximized (:simalt ~x, guarded by
has('gui_running')) and setsshortmess+=F. Two hex views side by side
want the width anyway, and a narrow window was what made Vim stop for a
hit-enter prompt on each file: a long path plus the size makes the file
message longer than one line, which is what triggers it. -
:HexPairGoOffsettakes$for the file's last byte, the same
shorthand:HexPairPageGotoalready took for the last page - the two
prompts sit under neighbouring keys (<Leader>band<Leader>g), and
answering one in the other's language should not be a mistake. -
The Vim requirement is stated exactly, and proved. It is Vim 8.0 at
any patch level — the old wording asked for patch 8.0.0794, which has not
been true sincecount()over a string left the plugin — plus Vim
9.0.0795 with+num64for the three writes that change a file's length.
It is no longer a claim: CI builds Vim 8.0.0000 from source on every push
and runs the whole suite against it, where it passes in full because the
suite asks that Vim what it can do and checks that the rest is refused.
The floor had rotted twice while checking it was a manual ritual.
Fixed
- The splice named the wrong Vim patch, by a major release. Shortening a
file,:w {file}and a grow with more than half the file behind it need
readblob()with an offset and a size, which is patch 9.0.0795 —
plain whole-filereadblob()is 8.2.2343, and 8.2.4906, which the gate
asked for, is an unrelated MS-Windows patch. Any Vim between the two was
told it could do such a write and then failed withE118: Too many arguments for function: readblobpart way through the copy. The gate, the
message and the documented requirement all say 9.0.0795 now. xxdwas located only when a page was opened, so any other route
into the readers died withE121: Undefined variableinstead of a
message namingxxd. It is resolved on demand now, and a missingxxd
is reported as itself wherever it is first needed.- On native Windows, everything past 2 GiB was read and written at the
wrong offset.xxdkeeps its seek offset in a Clong—strtol()
intolong seekoff, thenfseek()— and alongis 32 bits on Windows,
which is LLP64. Worse,strtol()saturates: an offset past the limit
does not fail, it silently becomes2147483647andxxdreads, or
writes, a page from there. So a 120 GiB file compared with a 77 GiB one
showed bytes as matching on pages wholly past the shorter file's end —
both were being read at 2 GiB, where the shorter one does have data —
while the same hexpair on the same files was right under WSL, where a
longis 64 bits.xxd -randxxd -oshare the limit, so the page you
look at, the bytes a diff compares and the bytes a:wputs back were
all affected. Everything past that limit now goes through PowerShell —
see Windows and the 2 GiB limit inREADME.md. - A nonsensical
g:hexpair_bytes_per_linewas accepted. Zero passed
the "page size must be a multiple of it" check, be...
Release v2.2.0
[v2.2.0] – 2026-08-28
Added
:HexPairSyncViewsbrings every scroll-bound view onto the byte this
one is on - the page holding it, the byte itself, and level.
'scrollbind'promises that windows move together, not that they are
on the same byte, and within a page they navigate independently on
purpose; this is the way back from moving the cursor by hand. (A jump
needs no help - see Fixed.)<Plug>(HexPairSyncViews),<Leader>=in
hexpair.vimrc.:HexPairInspectmarks the bytes it just read (HexPairInspect,
Visualby default), in both columns, for as long as the cursor stays
on the byte they were read from. The report's first line already says
which bytes it is about; the marking is what saves counting eight pairs
of digits back off a line of forty-eight. Near the end of a page or of
the file the report can only read three or four, and then three or four
are marked - a marking of eight would be saying something the report
does not.:HexPairInspect!takes it off at once,
g:hexpair_show_inspect = 0never draws it.:HexPairInsertChar [++enc={encoding}] {text}puts the bytes of a
character in at the cursor - the data inspector read backwards. It says
what the bytes at the cursor would be as utf-8, utf-16 and utf-32; this
writes a character in exactly those.Šisc5 a0, or60 01, or
00 00 01 60, and which of them 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, and
++enc=overrules it for a single insert. The bytes go in before the
byte under the cursor and push the page along, exactly as typing them
into the dump would, so oneutakes the insert back and nothing
reaches the file until:wdoes. utf-8, utf-16le/be, utf-32le/be,
latin1 and ascii are computed from the code point rather than converted,
because a Vim string cannot hold a NUL andAin utf-16le is41 00;
any other name is handed toiconv(), checked by converting it back,
and refused if this Vim does not know it.<Plug>(HexPairInsertChar)
asks for the text (<Leader>Iinhexpair.vimrc).docs/: the animation at the top ofREADME.md, and what records
it. The plugin at work on its own v2.1.0 release tarball, fetched
live: a reproducible build, so the bytes on screen are the bytes anybody
else gets, and the same 480 KiB can be fetched and followed along offset
for offset. A binary opened as plain text and then as a
hex page, the byte and its character lit up together as the cursor walks
the columns, searches by text and by bytes across the whole file, a byte
typed over and the ASCII column catching up on:HexPairRefresh, the two
bytes of a multi-byte character read back by the data inspector, a
character written in by its bytes, a write that says what a longer file
costs, andvimhexdiff's two panes with their differences marked - shown
through both the:HP*commands and the key mappings, because a recording
made only of typed commands reads as if the plugin had no keys, and with
every line left on the screen long enough to be read before it is sent.
It is recorded rather than drawn
(docs/hexpair-demo.sh), from this repository's own working tree, so
it can be made again whenever what it shows stops being true.
Changed
<Leader>Gis the suggested key for:HexPairPageGoto!- ask which
page to go to, discarding unwritten changes - wherehexpair.vimrcand
theREADME.mdexample both said<Leader>P. It belongs beside
<Leader>g, which is the same question without the bang, the way every
other pair here is a letter and its capital;Gwas the go-to-mark key
until v2.1.0 moved the marks under<Leader>m, and freeing it is what
the pair was waiting for. The plugin still defines no key mappings of
its own - this is the mapping file and the documentation.
Fixed
-
A jump takes the scroll-bound views along whether the page turned or
not. It used to do it only when the byte was on another page, so the
same keystroke moved the other window or left it behind depending on how
far it happened to go. A jump names a byte and a byte means the same
thing in every view of the file; moving the cursor by hand is the other
thing, and stays independent. -
vimhexdiffstarts with both cursors on the first difference, which
it has always said it does.'scrollbind'syncs movement made from the
moment a window was bound, and everythingvimhexdiffdoes happens
insideVimEnter- before the loop that would have done the syncing has
run even once. The left window jumped to the first difference and the
right one stayed at the top of page 1, showing a different part of a
different file. -
The question a length-changing write asks says which way the length
changed. A page that grows by more than the tail behind it is cheaper
to write the file afresh than to shift in place, and takes the same road
as a shortening - where it announced itself as "Shortening a file means
writing it afresh" about a file that was getting longer. -
A jump to a byte on another page keeps the scroll-bound windows
together. Invimhexdiff, walking the differences with
:HexPairDiffNext/:HexPairDiffPrevcame apart the moment a jump
crossed a page boundary: the other window turned to the right page but
stayed at its first byte, while this one went on to the byte it was
going to - two windows scrolling in step through different parts of two
files, which is the one thing'scrollbind'is there to prevent. A
page turn and the cursor's arrival are two steps, and the windows were
being levelled between them::syncbind, which is what tells Vim where
level is after a page has been loaded under a window, also swallows the
next scroll it would have followed - and that next scroll was this
window going to the byte. The levelling now happens after both steps,
and the bound window lands on the same byte rather than on the page's
first. Everything that goes to a byte is fixed by the same change:
:HexPairDiffNext/:HexPairDiffPrev,:HexPairFindand its
repeats,:HexPairGoOffset,:HexPairGoMarkand
:HexPairModifiedNext/:HexPairModifiedPrev. -
The progress line of a file-wide scan is readable, and
CTRL-Cstops
the scan. Neither was true: the line was echoed and then wiped by the
redraw meant to show it, dozens of times a second, so a search of a
large file looked like a hang with a flicker in the message line; and
every block read caught the interrupt along with everything else and
went on to read the next block, soCTRL-Conly worked if it landed in
the sliver of time between two reads. It stops the scan now, wherever
it is pressed, and answershexpair: stopped- a scan only reads, so
there is nothing to undo. The line also says how far it has got as a
size, not only as a percentage, because one per cent of a 70 GiB file
is 700 MB and minutes of reading, and a figure that does not move is a
figure that says "hung":hexpair: searching 2.3 GiB of 70.0 GiB (3%, CTRL-C stops) -
:HexPairDiffnames the file it is comparing against the short way
(~for your home directory, relative where it is below the working
directory), as the jump messages already did - and so does the one
jump message that still spelled it out in full, "file is longer:
its bytes from N on have nothing here to differ from", which of the
three is the longest and so the likeliest to wrap. Spelled out in full
the summary ran past the command line, wrapped, and cost a hit-enter
prompt - which is not only a keystroke: the prompt holds the screen as
it was, so the jump the next command made was not drawn until the
prompt was dismissed, and the view looked like it had ignored it.
Release v2.1.0
[v2.1.0] – 2026-08-24
Added
- Search across the file.
:HexPairFind {bytes}looks through the
whole file a block at a time and lands the cursor on the byte it found,
turning the page on the way; a pattern is bytes (de ad be ef,
deadbeef) and?stands for any nibble.:HexPairFindText {string}
searches for the bytes of a string,:HexPairFindNext/
:HexPairFindPrevrepeat it either way and obey'wrapscan', and
every match on the page is marked (HexPairFind) - including one that
straddles a page boundary, which is marked on both pages it touches.
/could never do this: it searches the page on screen, which is
a window on the file. A search reads the file a megabyte at a time, and
from 16 MB up says how far it has got, since a scan of a large file takes
long enough to look like a hang;CTRL-Cstops it, and nothing has been
changed by then.:HexPairDiffNextreports the same way. - Replacing what was found.
:HexPairReplace {bytes}over the match
under the cursor,:HexPairReplaceAllInPage {pattern} / {bytes}over
every match on the page in view - the scope is in the name, because
everything here writes one page at a time and a file-wide replace would
be a different mechanism, not a bigger version of this one. Both edit
the page exactly as typing over the dump would, so nothing reaches the
file until:wdoes, and a replacement of a different length asks the
same question any other insertion does. :HexPairDiff [file]marks every byte of the page that differs
from the same offset of another file (HexPairDiff) and says how many
differ;:HexPairDiffNext/:HexPairDiffPrevwalk the whole file for
the next change - a run of differing bytes is one change however
long it is, so the jumps move between changes rather than through the
bytes of one, and backwards lands on a change's first byte as[cdoes
in a diff.:HexPairDiff!stops comparing and clears the marking
(<Plug>(HexPairDiffClear), and<Plug>(HexPairFindClear)for
:HexPairFind!). The shell wrapper gainsvimhexdiff FILE1 FILE2,
which opens both side by side, each marking what differs from the
other, cursors on the first difference and the windows scroll-bound -
and a page turn in either window takes the other with it, to the page
holding the same byte, since'scrollbind'promises the two move
together and a page turn is the one kind of scrolling Vim cannot
follow on its own (g:hexpair_bind_pages).- Marks in the file:
:HexPairMark {name},:HexPairGoMark {name},
:HexPairMarks,:HexPairMarkDelete {name}, and the byte a mark
stands on underlined on the page (HexPairMark,
g:hexpair_show_marks). All of them are reachable from a key: the
three that need a name ask for it and complete the names that exist
(<Plug>(HexPairMark),<Plug>(HexPairMarkDelete),
<Plug>(HexPairGoMark)). 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 absolute byte offsets kept per file, shared by every
view of it. HexPairModified: the bytes edited and not yet written are marked
in both columns, so an edit in a dump no longer looks exactly like
everything around it (g:hexpair_show_modifiedturns it off). It links
toDiffChangerather than the closer-soundingDiffText, whose own
default is a red background with no foreground - black on red for
anyone with a light background.- The markings are drawn in the windowed text view too, one column
per byte where the dump gives a byte three: the bytes you edited, the
bytes that differ from the file being compared with, the matches of a
search, and the byte a mark stands on. The line break that ends a
text-view line is a byte of the page with no column of its own, and is
therefore the one byte never marked. :HexPairModifiedNext/:HexPairModifiedPrevwalk between the
runs of edited bytes the way:HexPairDiffNextwalks changes: bytes
that touch are one edit, the cursor lands on the first byte of each,
and the message says which of how many. No scan of the file is needed -
turning a page needs an unmodified buffer or a bang that discards it,
so bytes edited and not yet written only ever exist on the page in
view.hexpair.vimrc: the mappings the maintainer uses, shipped in the
repository and the release tarball so that a vimrc can source them
rather than copy them -runtime pack/*/start/hexpair/hexpair.vimrc,
one line that resolves on Linux, Windows and WSL alike because
'runtimepath'already names each platform's own per-user directory.
It never takes a key that is already mapped, and carries every option
and highlight group as a commented-out example.- Every command under a short name too:
:HPFind,:HPToggle,
:HPReplaceAllInPage- same arguments, same bang, same completion,
because:HexPair…is a lot to type at a:prompt.
g:hexpair_short_commands = 0leaves that namespace alone. - Prompting
<Plug>targets for the commands that take something
typed:<Plug>(HexPairFind),<Plug>(HexPairFindText)and the three
mark ones above, each completing what it can - the same shape
<Plug>(HexPairPageGoto)has had. The Visual-mode
<Plug>(HexPairSelection)also puts the selection back when it has
reported on it: asking about a selection from the:line is what
ends Visual mode, and losing it to look at it is not a trade worth
making. :HexPairGoOffset +N/-Nsteps from the byte the cursor is on,
crossing pages like a position does.:HexPairOpen!abandons a modified buffer in the window, the bang
README had documented for a year and the command never had.- A data inspector.
:HexPairInspect(<Plug>(HexPairInspect))
reads 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, plus
float32andfloat64, with the byte itself also shown as a
character, in binary and in octal - and what the bytes would be as
text: UTF-8, UTF-16 and UTF-32, each saying what is wrong with the
bytes (an overlong sequence, a lone surrogate, a value past U+10FFFF)
rather than reporting a code point for something that is not one. The
bytes are the page's, as the buffer holds them — edits included — and
stop at its end, where the wider rows say how many are left rather than
reaching into a page that is not on screen. :HexPairSelection(<Plug>(HexPairSelection), worth mapping in
Visual mode as well as Normal) says how many bytes a Visual selection
covers and which, 1-based like the banner, so the numbers can be typed
straight into:HexPairGoOffset. Asked from Visual mode it puts the
selection back and waits for a key, since Vim's own-- VISUAL --is
drawn over the message line the moment it gets there. A blockwise
selection, whose bytes are not one run, leads with the count and says
how many lines and how many per line.HexPairStatus()for'statusline':hex 3/349 @0x50a01 (330241)
in the hex view,txt 3/349 @0x50a01 (330241)in the text view, and
an empty string in every buffer hexpair has not touched, so one
statusline serves both. It never walks the page — it is called on every
cursor movement — and marks a page with unwritten edits with a+.g:hexpair_ruler(default 0): a ruler line between the banner and
the dump, numbering the byte columns — two digits over each hex byte,
the low nibble over each ASCII character. Like the banners it starts
with a"and therefore carries no bytes.:HexPairPageGototakes$and+N/-Nas well as a page
number, at the command line and at the<Plug>prompt alike — and
therefore invimhextoo:vimhex disk.img '$'opens the end of a
file without working out how many pages it has.:HexPairSplit [page]and:HexPairVSplit [page]: a second view
of the same file in a new window, showing a page named the way
:HexPairPageGotonames one and counted from the view you are in. The
two views share nothing but the file — each has its own buffer, page,
cursor and unwritten changes, and a:win either patches only the
page that view holds, so one region can be read while another is
edited. Previously a second:HexPairOpenof the same file failed with
E95, because the buffer's name was the file's alone; the second one
is now numbered (disk.img [hexpair page #2]).g:hexpair_split_views(default 0): with it set, a plain:split,
:vsplitor:tab splitof a hex page becomes an independent view of
the same file too, opened on the same page and byte — and a split of
the text view stays a text view. Left off by default, because a page is
thousands of lines and looking at two parts of one page in two windows
is what:splitis for everywhere else in Vim.- The page's own bytes are hashed when it is read and again before it
is patched, so a writer that changes bytes in place within the same
second — invisible to the file's size and modification time, which is
all a portable Vim can see — is caught rather than overwritten. It
costs one page read on either side (a page turn 13 ms → 26 ms, a
same-length write 129 ms → 144 ms at the default page size, both
independent of the size of the file). It also replaces the modification
time as the freshness test: what a write now asks is whether its own
page changed, not whether the file did, so a second view of the same
file — or any other process writing elsewhere in it — no longer locks a
write out. A file whose length changed is still refused outright,
since that moves every page after the change.
Ch...
Release v2.0.0
[v2.0.0] – 2026-08-21
Added
- Paged large-file mode, the reason for the major version:
:HexPairOpen {file} [page]shows one configurable-size page
(g:hexpair_page_size, default 128 KiB) of an arbitrarily large file
as a hex dump with absolute file offsets, without ever loading the
rest of the file into a buffer — usable straight from the shell,
e.g.vim -c 'HexPairOpen bigfile.bin'.:HexPairPageNext[!]/
:HexPairPagePrev[!]/:HexPairPageGoto[!] {N}navigate between
pages, refusing to discard unsaved changes without!;
:HexPairPagesreports the current page, total pages and byte
range. Each page is bracketed by a decorative banner line (page
number, byte range) highlighted via the newHexPairPageBanner
group.HexPairOpenFile({file} [, {page}])opens a page the same way as
:HexPairOpenbut as a direct function call, for scripts, mappings
or shell wrappers that build the filename programmatically — safer
than constructing an:HexPairOpencommand-line string for a name
containing a space or a literal$, which does not fully round-trip
through the Ex command's own argument parsing.<Plug>(HexPairPageGoto)
prompts for a page number instead of needing a typed:HexPairPageGoto {N};<Plug>(HexPairPageGotoForce)is the same prompt but discards
unsaved changes without asking, like the{N}variant with!. :HexPairRefresh(<Plug>(HexPairRefresh)): regenerate the offset
and ASCII columns from the current hex payload without writing to
disk — the same round trip a toggle off followed by a toggle on
would perform, but staying in hex mode. Validated like:w; an
invalid dump refuses the refresh instead of being converted. The
'modified'flag is unaffected — only the rendering changes, never
a byte of content.:won a paged view writes just that page, by one of three
mechanisms chosen by what the edit did to its length. An edit that
KEPT the length - overwriting values, the common case - patches the
page in place throughxxd -rwith the target as an argument: the
file keeps its length and every byte outside the page keeps its
content, at a cost independent of the file's size. An edit that
INSERTED bytes moves only what follows them - the tail is shifted
right in place withxxdand the page patched in, so the head of the
file is never even read, appending to the last page moves nothing at
all, and the temporary space needed is one block's worth of hex
whatever the file's size; the tail is moved from the end backwards, so
a byte is never overwritten before it has been copied and no second
copy of it is kept. An edit that DELETED bytes writes the file afresh,
because moving the tail left is the same operation but nothing in Vim
orxxdcan then shorten the file: head, edited page and tail are
block-copied (8 MiB blocks, so memory does not follow the file's size)
into a temporary file, which replaces the original by being copied
back over it, keeping its inode, owner and permissions - and if that
copy back fails part way through, the temporary file holds the
complete new content and its path is reported rather than deleted.
Either change of length says by how much the file will change and how
many bytes that writes, and asks first;g:hexpair_page_confirm = 0
answers yes automatically, for scripts. Before any of them the dump is
validated exactly as in the whole-file mode, and the file's size and
modification time are compared with what they were when the page was
read - a file that changed on disk meanwhile is refused rather than
patched at offsets that may no longer mean anything. Afterwards the
page is re-read from disk and the cursor returns to the byte it was
on, even when a splice moved every byte behind it; a shrinking write
that empties the file leaves a view saying so instead of a stale dump.- The
vimhexshell wrapper now ships ashexpair.bashrcin the plugin
directory, so it can be sourced from~/.bashrcrather than copied
out of the documentation:
source ~/.vim/pack/plugins/start/hexpair/hexpair.bashrc. It handles
-for standard input, a page number or an@BYTEposition, and
$VIMHEX_VIMpicks a particular Vim. :HexPairPagesalso reports the byte under the cursor, in hex with
the decimal in brackets and 1-based — exactly the form
:HexPairGoOffsetand thevimhexwrapper's@BYTEtake, so a
position can be written down and gone back to.- A Visual selection is mirrored in the other column, the way the
byte under the cursor already was: select hex digits and the text they
are is highlighted, select text and the bytes it is are highlighted.
Characterwise, linewise and blockwise selections all work, and one
spanning several lines is mirrored line by line. Only the part on
screen is mirrored, which keeps the work per cursor movement the same
however much of a page is selected. :HexPairGoOffset[!] {byte}jumps straight to a byte, decimal or
0x-prefixed, turning the page it falls on and leaving you in
whichever view you were in. The position is 1-based — byte 1 is the
file's first byte, the numbering the page banner and:HexPairPages
already use, so a number read off the banner can be typed back in. Pages are fixed-size slices, so the page
holding an offset is a division.<Plug>(HexPairGoOffset)prompts for
the offset the way<Plug>(HexPairPageGoto)prompts for a page;
<Plug>(HexPairGoOffsetForce)is the!variant.:w {file}on a paged view writes the entire content being paged,
with the current page's edits in it, to{file}, leaving the original
alone — a save-as rather than a refusal. For a view paged from piped
input (cat x | vim -) that is the only way to save at all, and the
view adopts{file}afterwards, so a later plain:wpatches pages
into it. hexpair also warns when it pages a buffer that was not read
in binary mode, since piped input cannot be re-read with++binthe
way a named file can.<Plug>(HexPairPages), so every command that takes no argument now
has a<Plug>target and can be bound to a key — the README and
:help hexpair-mappingslist the whole set on a<Leader>prefix.
Changed
- One hex mode, always paged.
:HexPairToggleno longer converts a
whole buffer: it shows one page, always with the banner — a small
file simply has exactly one — and toggles from there to a windowed
text view of the same page's raw bytes and back. There is
deliberately no way back to the plain buffer, since a buffer holding
one page is not the file and a plain:wwould truncate the file
down to it; every hex-mode buffer isbuftype=acwritewith the
page-range write path for the same reason. Where a toggled buffer's
pages come from depends on what it was: an unmodified file-backed
buffer is paged from its file, an unnamed one (cat x | vim -) from
a private temp it is spilled into, and a modified file-backed one is
refused — the buffer and the file disagree, and both ways of
resolving that lose edits quietly. - A file with no bytes is viewable - it simply has no pages and the view
says so - and:ere-reads the current page. - Pages are plain fixed-size slices: the paged view reads the width of
each line's offset column off the line itself, so a page may
span the point at 4 GiB wherexxdwidens that column from eight hex
digits to nine, instead of page boundaries being clamped to keep each
page uniform. PageNalways starts at(N-1) * g:hexpair_page_size,
page numbering no longer shifts when a file grows past 4 GiB, and a
bare hex line with no offset column at all is laid out correctly too. - Only a write that shortens a file - and
:w {file}, and a growing
write whose tail is more than half the file, both of which go through
the same splice - requires Vim patch 8.2.4906 with+num64, for
readblob(), checked when such a write is attempted and refusing just
that write. Viewing pages, navigating them, same-length writes and
inserts work on the Vim 8.0 baseline the rest of the plugin requires. - Writing a page walks it once instead of three times (validation,
cursor mapping and stripping share one scan): a write on a 128 KiB
page went from 0.5 s to 0.32 s, and the saving scales with the page
size.
Fixed
- An empty file grew to one byte when it went through the hex view.
Vim serializes an empty buffer for a filter as a single newline, so
the dump showed a0athe file did not contain and writing it back
created one. Deleting the whole dump and writing now also produces an
empty file rather than a one-byte one. A file that really holds a
lone0alooks identical in the buffer and still dumps that byte. - Data loss: toggling hex mode off used to unconditionally mirror the
buffer's modified state from BEFORE hex mode was entered, so an edit
made to the dump on an until-then-unmodified buffer silently cleared
'modified'on toggle-off —:qwould then discard it without a
warning. The buffer is now tracked for real content changes made
while in hex mode (viab:changedtick, unaffected by cursor
movement) independently of the pre-hex-mode state, in both
directions: a pre-existing unsaved change is still preserved, and an
edit made purely in hex mode now correctly marks the buffer modified
on toggle-off.
Release v1.1.0
[v1.1.0] – 2026-07-19
Added
-
Bundled filetype plugin (
ftplugin/xxd.vim) with dump-editing
defaults —tabstop=10,expandtab,shiftwidth=3(one hex byte),
no automatic formatting, wrapping or indenting — applied to any
buffer withfiletype=xxdand fully reverted viab:undo_ftplugin
when the hex view is toggled off (including buffers whose original
filetype was empty, where noFileTypeevent fires). Suppress with
let b:did_ftplugin = 1in a personalftplugin/xxd.vim, or
override individual settings inafter/ftplugin/xxd.vim
(:help hexpair-ftplugin). -
g:hexpair_paste(default on): the global'paste'option is
switched on while the cursor is in a hex-mode buffer and restored to
its previous value when the cursor leaves it or hex mode is toggled
off, so insert-mode mappings and abbreviations cannot mangle typed
hex; all other buffers keep the user's own'paste'state. The
buffer's'expandtab'is preserved across the switch. -
Dump validation: a character in the hex area that is not a hex
digit, or an odd total number of hex digits, now aborts:wand
hex-mode toggle-off with an error and the cursor parked on the
offender, instead of silently dropping data — the file on disk and
the dump keep their previous content. This also limits the damage
after anuthat undid the conversion itself: the non-dump content
is refused rather than converted. -
:e/:e!while hex mode is active now regenerates the dump from
the freshly read file and keeps hex mode and the cursor byte offset,
instead of leaving raw binary content in a buffer that still
believed it was a dump.
Changed
- Documented mapping examples now use the conventional
<Leader>
prefix instead of§, which only exists on some keyboard layouts
(e.g. Czech); the plugin still defines no mappings of its own. The
help and README now also explain the<Leader>/mapleaderand
<Plug>mechanisms for readers new to them.
Release v1.0.0
hexpair Vim plugin turns the classic :%!xxd workflow into a small, reliable
hex editor for Vim — pure VimScript plus the xxd utility that ships
with Vim, behaving identically on Linux, native Windows (Vim/gVim)
and WSL.
Highlights
- Live pair highlighting. The byte under the cursor is highlighted
in both columns of the dump: stand on a hex pair and the matching
ASCII character lights up, and vice versa. Direction-aware groups
(HexPairActivefor the cursor side,HexPairMirrorfor the
counterpart) show at a glance which column you are in. - Byte-exact cursor mapping. Toggling between the normal view and
the hex view keeps the cursor on the very same byte — exact even for
files with a BOM, CRLF or mixed CRLF/LF line endings, and
single-byte file encodings (utf-16 remains approximate). - Safe
:win hex mode. The buffer is transparently converted
back to binary before the write and the dump is regenerated
afterwards — the file on disk always receives the real bytes, never
the textual dump, and the cursor stays on its byte even when
insertions shifted all offsets. - Forgiving editing. Only the HEX column matters: the offset and
ASCII columns are purely decorative and are stripped before the
reverse conversion. Insert, delete or reorder lines freely —
inserted lines need no offset and no ASCII part; offsets and the
ASCII column self-heal on the next:w. - Column navigation.
:HexPairGoHex,:HexPairGoAsciiand
:HexPairSwapjump between the two representations of the byte
under the cursor. - Binary correctness. A file opened without
vim -bis reloaded
with:edit ++binon the first toggle, so the dump always shows the
exact on-disk bytes and a later:wcannot silently re-encode the
file. - No default key mappings. Commands and
<Plug>mappings only —
you choose your own keys.
Installation
mkdir -p ~/.vim/pack/plugins/start
tar xf hexpair.v1.0.0.tar -C ~/.vim/pack/plugins/start/
vim -c 'helptags ALL' -c 'q'Then add your own mappings to ~/.vimrc, for example:
nmap §h <Plug>(HexPairToggle) " toggle hex view
nmap §< <Plug>(HexPairGoHex) " jump to the HEX column (same byte)
nmap §> <Plug>(HexPairGoAscii) " jump to the ASCII column (same byte)
nmap §- <Plug>(HexPairSwap) " jump to the opposite columnFull documentation: :help hexpair after installation.
Requirements
- Vim 8.0+ (native packages; the plugin uses lambda expressions).
- The
xxdutility, which ships with Vim — looked up onPATHfirst
and then in$VIMRUNTIME, so on Windows the bundledxxd.exeis
found even when it is not onPATH.
Verifying the download
The release tarball is reproducible: packaging the sources of this
tag with ./pack-release (Linux, MacOS, ...) or .\pack-release.cmd (Windows)
produces a byte-identical archive on any platform, so its SHA-256 can be
independently re-derived. The tarball is signed with GPG key
489C 5EC8 0FD6 2BE8 9E59 B4F7 19C1 3E8C E0F5 DB61:
gpg --keyserver keys.openpgp.org --recv-keys 489C5EC80FD62BE89E59B4F719C13E8CE0F5DB61
gpg --verify hexpair.v1.0.0.tar.asc hexpair.v1.0.0.tarAbout this release
v1.0.0 is the first public release, consolidating fourteen internal
iterations that shaped the plugin before this import — the evolution
notes are in CHANGELOG.md.