From 59e2feee5b22223c47f63f8a0932c63dd66b4619 Mon Sep 17 00:00:00 2001 From: John Date: Wed, 1 Jul 2026 20:42:28 -0400 Subject: [PATCH 1/2] Adding an option for color profiles and a side-by-side mode --- CHANGES-SIDEBYSIDE.md | 83 ++++++++++ compile | 348 ++++++++++++++++++++++++++++++++++++++++++ config.h.in | 10 +- curses/ConWin.cpp | 234 ++++++++++++++++++++++++---- curses/ConWin.hpp | 10 +- tools/vbindiff.pod.tt | 52 ++++++- vbindiff-example.conf | 28 ++++ vbindiff.cpp | 245 +++++++++++++++++++++-------- 8 files changed, 901 insertions(+), 109 deletions(-) create mode 100644 CHANGES-SIDEBYSIDE.md create mode 100755 compile create mode 100644 vbindiff-example.conf diff --git a/CHANGES-SIDEBYSIDE.md b/CHANGES-SIDEBYSIDE.md new file mode 100644 index 0000000..806612a --- /dev/null +++ b/CHANGES-SIDEBYSIDE.md @@ -0,0 +1,83 @@ +# VBinDiff — Side-by-Side & Colorized Fork + +Based on madsen/vbindiff (the canonical upstream). Two changes from stock +VBinDiff: + +## 1. Side-by-side layout (opt-in via `-s`/`--side-by-side`) + +By default VBinDiff behaves exactly as before: two files stacked one above +the other, fits an 80-column terminal. + +Pass `-s` to instead show the two files side by side (left/right) with a +colored divider between them: + + vbindiff file1 file2 # default: stacked, like stock vbindiff + vbindiff -s file1 file2 # side by side, 16 bytes/line, needs ~155 cols + vbindiff -s -w 8 file1 file2 # side by side, 8 bytes/line, needs ~85 cols + +`-w`/`--width` sets bytes per line in either mode (default 16). It matters +most in side-by-side mode, where it's the main lever for fitting your +terminal width — each pane needs roughly `10 + 3*width + width/8 + width` +columns, so two panes plus a divider need about `2*that + 3`. + +If your terminal is too narrow for the requested width, VBinDiff exits +with a clear error telling you the required width, rather than corrupting +the display. + +## 2. Configurable colors (default matches original vbindiff) + +The default palette is unchanged from stock VBinDiff — the classic +white-on-blue theme, red for differences, yellow for edits, reverse-video +filename/mode bars. The one addition is the pane divider in side-by-side +mode, styled to match (white on blue, bold). + +Colors are fully configurable via a config file, if you want to move away +from the original look: + + vbindiff -c mytheme.conf file1 file2 + +Or drop a file at `~/.vbindiffrc` to have it load automatically every run +(silently ignored if it doesn't exist; parse errors in an explicit `-c` +file are reported and abort startup). + +See `vbindiff-example.conf` in this repo for the file format and a sample +"classic diff" green/red theme. + +## Implementation notes + +- `lineWidth` (bytes/line) is now a runtime value instead of a compile-time + constant, so the file-buffer indexing was changed from a 2D array trick + to explicit `row*lineWidth + col` arithmetic. +- `sideBySide` (default false) gates between the original stacked layout + code path and the new side-by-side one throughout `calcScreenLayout()`, + `initialize()`, and `positionInWin()`. +- Screen layout (`leftMar2`, `paneWidth`, `screenWidth`) is computed from + `lineWidth`/`sideBySide` at startup via `computeLayoutMetrics()`. +- Colors moved from 4 hardcoded curses color pairs to one configurable + `{fg, bg, attrs}` triple per UI style, loaded into curses color pairs at + startup and optionally overridden by a config file before that. +- The Goto/Find popup windows position themselves relative to the left + pane, right pane, or centered across both in side-by-side mode; in + stacked mode they use the original top/bottom vertical positioning. +- Gotcha fixed along the way: a `GetOpt` `ArgFunc` callback for a no-argument + flag must return `false` (not `true`) when it doesn't consume an + argument, or the library will swallow the next positional argument + (e.g. your first filename) as if it belonged to the flag. + +## Man page + +`tools/vbindiff.pod.tt` (the POD-in-Template-Toolkit source `vbindiff.1` is +generated from) has been updated to document `-s`, `-w`, and `-c`, with a +short "Side by side mode" and "Color configuration" section. Building it +requires the `libtemplate-perl` package (`Template.pm`); if that's not +installed, `make` still builds the `vbindiff` binary fine and just skips +the man page. + +## Build + +Same as upstream: + + git submodule update --init --recursive # fetches GetOpt dependency + autoreconf -i + ./configure + make diff --git a/compile b/compile new file mode 100755 index 0000000..df363c8 --- /dev/null +++ b/compile @@ -0,0 +1,348 @@ +#! /bin/sh +# Wrapper for compilers which do not understand '-c -o'. + +scriptversion=2018-03-07.03; # UTC + +# Copyright (C) 1999-2021 Free Software Foundation, Inc. +# Written by Tom Tromey . +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program that contains a +# configuration script generated by Autoconf, you may include it under +# the same distribution terms that you use for the rest of that program. + +# This file is maintained in Automake, please report +# bugs to or send patches to +# . + +nl=' +' + +# We need space, tab and new line, in precisely that order. Quoting is +# there to prevent tools from complaining about whitespace usage. +IFS=" "" $nl" + +file_conv= + +# func_file_conv build_file lazy +# Convert a $build file to $host form and store it in $file +# Currently only supports Windows hosts. If the determined conversion +# type is listed in (the comma separated) LAZY, no conversion will +# take place. +func_file_conv () +{ + file=$1 + case $file in + / | /[!/]*) # absolute file, and not a UNC file + if test -z "$file_conv"; then + # lazily determine how to convert abs files + case `uname -s` in + MINGW*) + file_conv=mingw + ;; + CYGWIN* | MSYS*) + file_conv=cygwin + ;; + *) + file_conv=wine + ;; + esac + fi + case $file_conv/,$2, in + *,$file_conv,*) + ;; + mingw/*) + file=`cmd //C echo "$file " | sed -e 's/"\(.*\) " *$/\1/'` + ;; + cygwin/* | msys/*) + file=`cygpath -m "$file" || echo "$file"` + ;; + wine/*) + file=`winepath -w "$file" || echo "$file"` + ;; + esac + ;; + esac +} + +# func_cl_dashL linkdir +# Make cl look for libraries in LINKDIR +func_cl_dashL () +{ + func_file_conv "$1" + if test -z "$lib_path"; then + lib_path=$file + else + lib_path="$lib_path;$file" + fi + linker_opts="$linker_opts -LIBPATH:$file" +} + +# func_cl_dashl library +# Do a library search-path lookup for cl +func_cl_dashl () +{ + lib=$1 + found=no + save_IFS=$IFS + IFS=';' + for dir in $lib_path $LIB + do + IFS=$save_IFS + if $shared && test -f "$dir/$lib.dll.lib"; then + found=yes + lib=$dir/$lib.dll.lib + break + fi + if test -f "$dir/$lib.lib"; then + found=yes + lib=$dir/$lib.lib + break + fi + if test -f "$dir/lib$lib.a"; then + found=yes + lib=$dir/lib$lib.a + break + fi + done + IFS=$save_IFS + + if test "$found" != yes; then + lib=$lib.lib + fi +} + +# func_cl_wrapper cl arg... +# Adjust compile command to suit cl +func_cl_wrapper () +{ + # Assume a capable shell + lib_path= + shared=: + linker_opts= + for arg + do + if test -n "$eat"; then + eat= + else + case $1 in + -o) + # configure might choose to run compile as 'compile cc -o foo foo.c'. + eat=1 + case $2 in + *.o | *.[oO][bB][jJ]) + func_file_conv "$2" + set x "$@" -Fo"$file" + shift + ;; + *) + func_file_conv "$2" + set x "$@" -Fe"$file" + shift + ;; + esac + ;; + -I) + eat=1 + func_file_conv "$2" mingw + set x "$@" -I"$file" + shift + ;; + -I*) + func_file_conv "${1#-I}" mingw + set x "$@" -I"$file" + shift + ;; + -l) + eat=1 + func_cl_dashl "$2" + set x "$@" "$lib" + shift + ;; + -l*) + func_cl_dashl "${1#-l}" + set x "$@" "$lib" + shift + ;; + -L) + eat=1 + func_cl_dashL "$2" + ;; + -L*) + func_cl_dashL "${1#-L}" + ;; + -static) + shared=false + ;; + -Wl,*) + arg=${1#-Wl,} + save_ifs="$IFS"; IFS=',' + for flag in $arg; do + IFS="$save_ifs" + linker_opts="$linker_opts $flag" + done + IFS="$save_ifs" + ;; + -Xlinker) + eat=1 + linker_opts="$linker_opts $2" + ;; + -*) + set x "$@" "$1" + shift + ;; + *.cc | *.CC | *.cxx | *.CXX | *.[cC]++) + func_file_conv "$1" + set x "$@" -Tp"$file" + shift + ;; + *.c | *.cpp | *.CPP | *.lib | *.LIB | *.Lib | *.OBJ | *.obj | *.[oO]) + func_file_conv "$1" mingw + set x "$@" "$file" + shift + ;; + *) + set x "$@" "$1" + shift + ;; + esac + fi + shift + done + if test -n "$linker_opts"; then + linker_opts="-link$linker_opts" + fi + exec "$@" $linker_opts + exit 1 +} + +eat= + +case $1 in + '') + echo "$0: No command. Try '$0 --help' for more information." 1>&2 + exit 1; + ;; + -h | --h*) + cat <<\EOF +Usage: compile [--help] [--version] PROGRAM [ARGS] + +Wrapper for compilers which do not understand '-c -o'. +Remove '-o dest.o' from ARGS, run PROGRAM with the remaining +arguments, and rename the output as expected. + +If you are trying to build a whole package this is not the +right script to run: please start by reading the file 'INSTALL'. + +Report bugs to . +EOF + exit $? + ;; + -v | --v*) + echo "compile $scriptversion" + exit $? + ;; + cl | *[/\\]cl | cl.exe | *[/\\]cl.exe | \ + icl | *[/\\]icl | icl.exe | *[/\\]icl.exe ) + func_cl_wrapper "$@" # Doesn't return... + ;; +esac + +ofile= +cfile= + +for arg +do + if test -n "$eat"; then + eat= + else + case $1 in + -o) + # configure might choose to run compile as 'compile cc -o foo foo.c'. + # So we strip '-o arg' only if arg is an object. + eat=1 + case $2 in + *.o | *.obj) + ofile=$2 + ;; + *) + set x "$@" -o "$2" + shift + ;; + esac + ;; + *.c) + cfile=$1 + set x "$@" "$1" + shift + ;; + *) + set x "$@" "$1" + shift + ;; + esac + fi + shift +done + +if test -z "$ofile" || test -z "$cfile"; then + # If no '-o' option was seen then we might have been invoked from a + # pattern rule where we don't need one. That is ok -- this is a + # normal compilation that the losing compiler can handle. If no + # '.c' file was seen then we are probably linking. That is also + # ok. + exec "$@" +fi + +# Name of file we expect compiler to create. +cofile=`echo "$cfile" | sed 's|^.*[\\/]||; s|^[a-zA-Z]:||; s/\.c$/.o/'` + +# Create the lock directory. +# Note: use '[/\\:.-]' here to ensure that we don't use the same name +# that we are using for the .o file. Also, base the name on the expected +# object file name, since that is what matters with a parallel build. +lockdir=`echo "$cofile" | sed -e 's|[/\\:.-]|_|g'`.d +while true; do + if mkdir "$lockdir" >/dev/null 2>&1; then + break + fi + sleep 1 +done +# FIXME: race condition here if user kills between mkdir and trap. +trap "rmdir '$lockdir'; exit 1" 1 2 15 + +# Run the compile. +"$@" +ret=$? + +if test -f "$cofile"; then + test "$cofile" = "$ofile" || mv "$cofile" "$ofile" +elif test -f "${cofile}bj"; then + test "${cofile}bj" = "$ofile" || mv "${cofile}bj" "$ofile" +fi + +rmdir "$lockdir" +exit $ret + +# Local Variables: +# mode: shell-script +# sh-indentation: 2 +# eval: (add-hook 'before-save-hook 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC0" +# time-stamp-end: "; # UTC" +# End: diff --git a/config.h.in b/config.h.in index f98ea63..f5bd7c1 100644 --- a/config.h.in +++ b/config.h.in @@ -15,9 +15,6 @@ /* Define to 1 if you have the header file. */ #undef HAVE_LIMITS_H -/* Define to 1 if you have the header file. */ -#undef HAVE_MEMORY_H - /* Define to 1 if you have the `memset' function. */ #undef HAVE_MEMSET @@ -30,6 +27,9 @@ /* Define to 1 if you have the header file. */ #undef HAVE_STDINT_H +/* Define to 1 if you have the header file. */ +#undef HAVE_STDIO_H + /* Define to 1 if you have the header file. */ #undef HAVE_STDLIB_H @@ -84,7 +84,9 @@ /* Define to the version of this package. */ #undef PACKAGE_VERSION -/* Define to 1 if you have the ANSI C header files. */ +/* Define to 1 if all of the C90 standard headers exist (not just the ones + required in a freestanding environment). This macro is provided for + backward compatibility; new code need not use it. */ #undef STDC_HEADERS /* Version number of package */ diff --git a/curses/ConWin.cpp b/curses/ConWin.cpp index af73f29..006831b 100644 --- a/curses/ConWin.cpp +++ b/curses/ConWin.cpp @@ -20,41 +20,205 @@ //-------------------------------------------------------------------- #include +#include +#include + +#include +#include +#include +#include +#include +using namespace std; #include "ConWin.hpp" void exitMsg(int status, const char* message); // From vbindiff.cpp -enum ColorPair { - pairWhiteBlue= 1, - pairWhiteBlack, - pairRedBlue, - pairYellowBlue +//-------------------------------------------------------------------- +// Default ("modernized") color scheme: +// +// Each Style gets its own foreground, background, and extra attributes +// (currently just A_BOLD). This can be overridden at runtime with +// ConWindow::loadColorConfig() -- see that function for the file format. + +struct StyleColor { short fg, bg; attr_t attrs; }; + +static StyleColor styleColor[cNumStyles] = { + { COLOR_WHITE, COLOR_BLUE, 0 }, // cBackground + { COLOR_WHITE, COLOR_BLUE, 0 }, // cPromptWin + { COLOR_WHITE, COLOR_BLUE, A_BOLD }, // cPromptKey + { COLOR_WHITE, COLOR_BLUE, A_BOLD }, // cPromptBdr + { COLOR_WHITE, COLOR_BLACK, A_REVERSE }, // cCurrentMode + { COLOR_WHITE, COLOR_BLACK, A_REVERSE }, // cFileName + { COLOR_WHITE, COLOR_BLUE, 0 }, // cFileWin + { COLOR_RED, COLOR_BLUE, A_BOLD }, // cFileDiff + { COLOR_YELLOW, COLOR_BLUE, A_BOLD }, // cFileEdit + { COLOR_WHITE, COLOR_BLUE, A_BOLD }, // cDivider (new in this fork; matches original border style) }; -static const ColorPair colorStyle[] = { - pairWhiteBlue, // cBackground - pairWhiteBlue, // cPromptWin - pairWhiteBlue, // cPromptKey - pairWhiteBlue, // cPromptBdr - pairWhiteBlack, // cCurrentMode - pairWhiteBlack, // cFileName - pairWhiteBlue, // cFileWin - pairRedBlue, // cFileDiff - pairYellowBlue // cFileEdit +static const char* const styleName[cNumStyles] = { + "background", + "promptWin", + "promptKey", + "promptBdr", + "currentMode", + "fileName", + "fileWin", + "fileDiff", + "fileEdit", + "divider", }; -static const attr_t attribStyle[] = { - COLOR_PAIR(colorStyle[ cBackground ]), - COLOR_PAIR(colorStyle[ cPromptWin ]), - A_BOLD | COLOR_PAIR(colorStyle[ cPromptKey ]), - A_BOLD | COLOR_PAIR(colorStyle[ cPromptBdr ]), - A_REVERSE | COLOR_PAIR(colorStyle[ cCurrentMode]), - A_REVERSE | COLOR_PAIR(colorStyle[ cFileName ]), - COLOR_PAIR(colorStyle[ cFileWin ]), - A_BOLD | COLOR_PAIR(colorStyle[ cFileDiff ]), - A_BOLD | COLOR_PAIR(colorStyle[ cFileEdit ]) -}; +static attr_t attribStyle[cNumStyles]; +static short pairOf[cNumStyles]; // curses color-pair number per style, 0 = none + +//-------------------------------------------------------------------- +// Parse a color name (case-insensitive) into a curses COLOR_* value. +// Returns true and sets `color` on success. + +static bool parseColorName(const string& name, short& color) +{ + static const struct { const char* name; short color; } table[] = { + { "black", COLOR_BLACK }, + { "red", COLOR_RED }, + { "green", COLOR_GREEN }, + { "yellow", COLOR_YELLOW }, + { "blue", COLOR_BLUE }, + { "magenta", COLOR_MAGENTA }, + { "cyan", COLOR_CYAN }, + { "white", COLOR_WHITE }, + { NULL, 0 } + }; + + string lower(name); + for (string::size_type i = 0; i < lower.size(); ++i) + lower[i] = tolower(static_cast(lower[i])); + + for (int i = 0; table[i].name; ++i) + if (lower == table[i].name) { + color = table[i].color; + return true; + } + + return false; +} // end parseColorName + +//-------------------------------------------------------------------- +// Load a color scheme from a config file. +// +// File format (one style per line, blank lines and lines starting with +// '#' are ignored): +// +// styleName = foreground/background[/bold] +// +// Valid styleNames: background, promptWin, promptKey, promptBdr, +// currentMode, fileName, fileWin, fileDiff, fileEdit, divider +// Valid colors: black, red, green, yellow, blue, magenta, cyan, white +// +// Example: +// fileDiff = red/black/bold +// fileEdit = yellow/black/bold +// +// Must be called before ConWindow::startup(). + +string ConWindow::loadColorConfig(const char* path, bool optional) +{ + ifstream in(path); + if (!in) { + if (optional) return ""; + return string("Unable to open color config file: ") + path; + } + + map nameToIndex; + for (int i = 0; i < cNumStyles; ++i) + nameToIndex[styleName[i]] = i; + + string line; + int lineNum = 0; + while (getline(in, line)) { + ++lineNum; + + // Strip comments & whitespace: + string::size_type hash = line.find('#'); + if (hash != string::npos) line.erase(hash); + + string::size_type start = line.find_first_not_of(" \t\r\n"); + if (start == string::npos) continue; // blank line + string::size_type end = line.find_last_not_of(" \t\r\n"); + line = line.substr(start, end - start + 1); + if (line.empty()) continue; + + string::size_type eq = line.find('='); + if (eq == string::npos) { + ostringstream err; + err << path << ':' << lineNum << ": expected 'name = fg/bg'"; + return err.str(); + } + + string name = line.substr(0, eq); + string value = line.substr(eq+1); + + string::size_type nEnd = name.find_last_not_of(" \t"); + name = name.substr(0, nEnd+1); + string::size_type vStart = value.find_first_not_of(" \t"); + if (vStart != string::npos) value = value.substr(vStart); + + map::iterator it = nameToIndex.find(name); + if (it == nameToIndex.end()) { + ostringstream err; + err << path << ':' << lineNum << ": unknown style name '" << name + << "'"; + return err.str(); + } + + // Split value on '/': + vector parts; + string::size_type pos = 0; + while (true) { + string::size_type slash = value.find('/', pos); + parts.push_back(value.substr(pos, slash - pos)); + if (slash == string::npos) break; + pos = slash + 1; + } + + if (parts.size() < 2) { + ostringstream err; + err << path << ':' << lineNum + << ": expected 'foreground/background[/bold]'"; + return err.str(); + } + + short fg, bg; + if (!parseColorName(parts[0], fg) || !parseColorName(parts[1], bg)) { + ostringstream err; + err << path << ':' << lineNum << ": unrecognized color name"; + return err.str(); + } + + attr_t attrs = 0; + for (size_t i = 2; i < parts.size(); ++i) { + string opt(parts[i]); + for (string::size_type j = 0; j < opt.size(); ++j) + opt[j] = tolower(static_cast(opt[j])); + if (opt == "bold") attrs |= A_BOLD; + else if (opt == "reverse") attrs |= A_REVERSE; + else if (opt == "underline") attrs |= A_UNDERLINE; + else { + ostringstream err; + err << path << ':' << lineNum << ": unrecognized attribute '" + << parts[i] << "'"; + return err.str(); + } + } + + int idx = it->second; + styleColor[idx].fg = fg; + styleColor[idx].bg = bg; + styleColor[idx].attrs = attrs; + } // end while getline + + return ""; +} // end ConWindow::loadColorConfig //==================================================================== // Class ConWindow: @@ -83,10 +247,18 @@ bool ConWindow::startup() if (has_colors()) { start_color(); - init_pair(pairWhiteBlue, COLOR_WHITE, COLOR_BLUE); - init_pair(pairWhiteBlack, COLOR_WHITE, COLOR_BLACK); - init_pair(pairRedBlue, COLOR_RED, COLOR_BLUE); - init_pair(pairYellowBlue, COLOR_YELLOW, COLOR_BLUE); + for (int i = 0; i < cNumStyles; ++i) { + short pair = short(i + 1); + init_pair(pair, styleColor[i].fg, styleColor[i].bg); + pairOf[i] = pair; + attribStyle[i] = styleColor[i].attrs | COLOR_PAIR(pair); + } + } else { + // No color support: fall back to plain attributes only: + for (int i = 0; i < cNumStyles; ++i) { + pairOf[i] = 0; + attribStyle[i] = styleColor[i].attrs; + } } // end if terminal has color return true; @@ -197,7 +369,7 @@ void ConWindow::close() void ConWindow::putAttribs(short x, short y, Style color, short count) { - mvwchgat(win, y, x, count, attribStyle[color], colorStyle[color], NULL); + mvwchgat(win, y, x, count, attribStyle[color], pairOf[color], NULL); touchwin(win); } // end ConWindow::putAttribs diff --git a/curses/ConWin.hpp b/curses/ConWin.hpp index 6ad2989..f77eae9 100644 --- a/curses/ConWin.hpp +++ b/curses/ConWin.hpp @@ -24,6 +24,7 @@ #define INCLUDED_CONWIN_HPP #include +#include #undef border // It interferes with my member function #define KEY_ESCAPE 0x1B @@ -40,7 +41,9 @@ enum Style { cFileName, cFileWin, cFileDiff, - cFileEdit + cFileEdit, + cDivider, + cNumStyles // Must be last }; class ConWindow @@ -76,6 +79,11 @@ class ConWindow static void showCursor(bool insert=true) { curs_set(insert ? 1 : 2); }; static void shutdown(); static bool startup(); + + // Load a color scheme from a config file (see ConWin.cpp for format). + // Returns "" on success, or an error message on failure. + // If `optional` is true, a missing file is not an error. + static std::string loadColorConfig(const char* path, bool optional); }; // end ConWindow #endif // INCLUDED_CONWIN_HPP diff --git a/tools/vbindiff.pod.tt b/tools/vbindiff.pod.tt index e71b42b..6550602 100644 --- a/tools/vbindiff.pod.tt +++ b/tools/vbindiff.pod.tt @@ -32,7 +32,7 @@ vbindiff - hexadecimal file display and comparison =head1 SYNOPSIS -B I [ I ] +B [I] I [ I ] =head1 DESCRIPTION @@ -45,8 +45,8 @@ large files (up to 4 GB). Movement Keys ------------- - Up Move one line (16 bytes) towards the beginning of the file - Down Move one line (16 bytes) towards the end of the file + Up Move one line (16 bytes by default; see -w) towards the beginning of the file + Down Move one line (16 bytes by default; see -w) towards the end of the file Left Move one byte towards the beginning of the file Right Move one byte towards the end of the file PageUp Move one page towards the beginning of the file @@ -125,9 +125,49 @@ Also, you cannot insert or delete bytes, only change them. =head1 OPTIONS - -L, --license Display license information for vbindiff - -V, --version Display the version number - --help Display help information + -L, --license Display license information for vbindiff + -V, --version Display the version number + --help Display help information + -s, --side-by-side Show files side by side instead of stacked + -w, --width=N Bytes shown per line (default 16) + -c, --config=FILE Load a color scheme from FILE + +=head2 Side by side mode + +By default, VBinDiff stacks the two files one above the other, as in +earlier versions. Passing C<-s> instead shows them side by side, left +and right, separated by a colored divider. + +Side by side mode needs a wider terminal, since both files' hex/ASCII +columns must fit on screen at once. Use C<-w> to show fewer bytes per +line if your terminal isn't wide enough; VBinDiff will report the +required width if the terminal is too narrow for the requested +combination of C<-s> and C<-w>. + +C<-w> also works without C<-s>, changing how many bytes are shown per +line in the normal stacked display. + +=head2 Color configuration + +VBinDiff's colors can be customized with a simple text config file, +loaded with C<-c> I, or automatically from C<~/.vbindiffrc> if +that file exists and no C<-c> option is given. + +Each line has the form: + + styleName = foreground/background[/bold|reverse|underline] + +Valid style names: C, C, C, +C, C, C, C, C, +C, C. + +Valid colors: C, C, C, C, C, +C, C, C. + +Blank lines and lines starting with C<#> are ignored. For example: + + fileDiff = green/black/bold + divider = magenta/black/bold =head1 BUGS diff --git a/vbindiff-example.conf b/vbindiff-example.conf new file mode 100644 index 0000000..20accec --- /dev/null +++ b/vbindiff-example.conf @@ -0,0 +1,28 @@ +# Example VBinDiff color config file. +# Copy to ~/.vbindiffrc to load automatically, or pass explicitly with +# -c/--config. +# +# Format: styleName = foreground/background[/bold|reverse|underline] +# +# Styles: +# background Base color used before anything else is drawn +# promptWin Background of the help bar at the bottom of the screen +# promptKey Highlighted key letters in the help bar (e.g. "F" in "F find") +# promptBdr Border/title text of popup windows (Goto, Find) +# currentMode Highlighted ASCII/EBCDIC indicator +# fileName The filename bar at the top of each pane +# fileWin The hex/ASCII body of each pane +# fileDiff Bytes that differ between the two files +# fileEdit Bytes you've changed while editing +# divider The vertical divider between the two side-by-side panes +# +# Colors: black, red, green, yellow, blue, magenta, cyan, white + +# --- Classic diff style: red = changed bytes, green = accent --- +fileDiff = red/black/bold +fileEdit = yellow/black/bold +divider = white/black/bold +fileName = black/green/bold +promptKey = green/black/bold +promptBdr = white/black/bold +currentMode = black/green/bold diff --git a/vbindiff.cpp b/vbindiff.cpp index 5c29aad..64e66cb 100644 --- a/vbindiff.cpp +++ b/vbindiff.cpp @@ -115,13 +115,15 @@ const Command cmToggleASCII = 12; const Command cmFind = 16; // Commands 16-19 const short leftMar = 11; // Starting column of hex display -const short leftMar2 = 61; // Starting column of ASCII display +short leftMar2 = 61; // Starting column of ASCII display (computed) -const int lineWidth = 16; // Number of bytes displayed per line +int lineWidth = 16; // Number of bytes displayed per line (-w) const int promptHeight = 4; // Height of prompt window const int inWidth = 10; // Width of input window (excluding border) -const int screenWidth = 80; +const int paneGap = 3; // Columns of divider between side-by-side panes +int paneWidth = 77; // Width of a single file pane (computed) +int screenWidth = 155; // Total width required (computed) const int maxPath = 260; @@ -139,11 +141,10 @@ void showPrompt(); class Difference; -union FileBuffer -{ - Byte line[1][lineWidth]; - Byte buffer[lineWidth]; -}; // end FileBuffer +// A file's buffer is a flat array of `bufSize` bytes, addressed as +// bufSize = numLines * lineWidth. Since lineWidth is now chosen at +// runtime (see --width), we can't use a compile-time-sized 2D array, +// so row/column access is done as data[row*lineWidth + col]. class FileDisplay { @@ -151,24 +152,25 @@ class FileDisplay protected: int bufContents; - FileBuffer* data; + Byte* data; const Difference* diffs; File file; char fileName[maxPath]; FPos offset; ConWindow win; bool writable; + int xPos; int yPos; public: FileDisplay(); ~FileDisplay(); - void init(int y, const Difference* aDiff=NULL, + void init(int x, int y, const Difference* aDiff=NULL, const char* aFileName=NULL); void resize(); void shutDown(); void display(); bool edit(const FileDisplay* other); - const Byte* getBuffer() const { return data->buffer; }; + const Byte* getBuffer() const { return data; }; void move(int step) { moveTo(offset + step); }; void moveTo(FPos newOffset); bool moveTo(const Byte* searchFor, int searchLen); @@ -183,7 +185,7 @@ class Difference friend void FileDisplay::display(); protected: - FileBuffer* data; + Byte* data; const FileDisplay* file1; const FileDisplay* file2; int numDiffs; @@ -227,17 +229,21 @@ class InputManager String lastSearch; StrVec hexSearchHistory, textSearchHistory, positionHistory; -ConWindow promptWin,inWin; +ConWindow promptWin,inWin,dividerWin; FileDisplay file1, file2; Difference diffs(&file1, &file2); const char* displayTable = asciiDisplayTable; const char* program_name; // Name under which this program was invoked LockState lockState = lockNeither; bool singleFile = false; +bool sideBySide = false; // -s/--side-by-side + +long optLineWidth = 16; // -w/--width +const char* optConfigPath = NULL; // -c/--config int numLines = 9; // Number of lines of each file to display int bufSize = numLines * lineWidth; -int linesBetween = 1; // Number of lines of padding between files +int linesBetween = 1; // Number of lines of padding between files (stacked mode) // The number of bytes to move for each possible step size: // See cmmMoveByte, cmmMoveLine, cmmMovePage @@ -317,19 +323,19 @@ int Difference::compute() // We return 1 so that cmNextDiff won't keep searching: return (file1->bufContents ? 1 : -1); - memset(data->buffer, 0, bufSize); // Clear the difference table + memset(data, 0, bufSize); // Clear the difference table int different = 0; - const Byte* buf1 = file1->data->buffer; - const Byte* buf2 = file2->data->buffer; + const Byte* buf1 = file1->data; + const Byte* buf2 = file2->data; int size = min(file1->bufContents, file2->bufContents); int i; for (i = 0; i < size; i++) if (*(buf1++) != *(buf2++)) { - data->buffer[i] = true; + data[i] = true; ++different; } @@ -339,7 +345,7 @@ int Difference::compute() // One buffer has more data than the other: different += size - i; for (; i < size; i++) - data->buffer[i] = true; // These bytes are only in 1 buffer + data[i] = true; // These bytes are only in 1 buffer } else if (!size) return -1; // Both buffers are empty @@ -356,7 +362,7 @@ void Difference::resize() if (data) delete [] reinterpret_cast(data); - data = reinterpret_cast(new Byte[bufSize]); + data = new Byte[bufSize]; } // end Difference::resize //==================================================================== @@ -389,6 +395,7 @@ FileDisplay::FileDisplay() diffs(NULL), offset(0), writable(false), + xPos(0), yPos(0) { fileName[0] = '\0'; @@ -404,13 +411,15 @@ FileDisplay::FileDisplay() // aDiff: The Difference object related to this buffer // aFileName: The name of the file to display -void FileDisplay::init(int y, const Difference* aDiff, +void FileDisplay::init(int x, int y, const Difference* aDiff, const char* aFileName) { diffs = aDiff; + xPos = x; yPos = y; - win.init(0,y, screenWidth, (numLines + 1 + ((y==0) ? linesBetween : 0)), + win.init(x, y, paneWidth, + numLines + 1 + ((y==0 && !sideBySide) ? linesBetween : 0), cFileWin); resize(); @@ -435,7 +444,7 @@ void FileDisplay::resize() if (data) delete [] reinterpret_cast(data); - data = reinterpret_cast(new Byte[bufSize]); + data = new Byte[bufSize]; // FIXME resize window } // end FileDisplay::resize @@ -460,17 +469,18 @@ void FileDisplay::display() FPos lineOffset = offset; short i,j,index,lineLength; - char buf[lineWidth + lineWidth/8 + 1]; - buf[sizeof(buf)-1] = '\0'; + const int hexGroups = (lineWidth + 7) / 8; + + vector buf(lineWidth + hexGroups + 1); + buf[buf.size()-1] = '\0'; - char buf2[screenWidth+1]; - buf2[screenWidth] = '\0'; + vector buf2(paneWidth + 1); + buf2[paneWidth] = '\0'; - memset(buf, ' ', sizeof(buf)-1); + memset(&buf[0], ' ', buf.size()-1); for (i = 0; i < numLines; i++) { -// cerr << i << '\n'; - char* str = buf2; + char* str = &buf2[0]; str += sprintf(str, "%04X %04X:",Word(lineOffset>>16),Word(lineOffset&0xFFFF)); @@ -481,22 +491,22 @@ void FileDisplay::display() *(str++) = ' '; ++index; } - str += sprintf(str, "%02X ", data->line[i][j]); + str += sprintf(str, "%02X ", data[(i)*lineWidth+(j)]); - buf[index++] = displayTable[data->line[i][j]]; + buf[index++] = displayTable[data[(i)*lineWidth+(j)]]; } if (index < 0) index = 0; // in case nothing was printed in this line - memset(buf + index, ' ', sizeof(buf) - index - 1); - memset(str, ' ', screenWidth - (str - buf2)); + memset(&buf[0] + index, ' ', buf.size() - index - 1); + memset(str, ' ', paneWidth - (str - &buf2[0])); - win.put(0,i+1, buf2); - win.put(leftMar2,i+1, buf); + win.put(0,i+1, &buf2[0]); + win.put(leftMar2,i+1, &buf[0]); if (diffs) for (j = 0; j < lineWidth; j++) - if (diffs->data->line[i][j]) { - win.putAttribs(j*3 + leftMar + (j>7),i+1, cFileDiff,2); - win.putAttribs(j + leftMar2 + (j>7),i+1, cFileDiff,1); + if (diffs->data[(i)*lineWidth+(j)]) { + win.putAttribs(j*3 + leftMar + (j/8),i+1, cFileDiff,2); + win.putAttribs(j + leftMar2 + (j/8),i+1, cFileDiff,1); } lineOffset += lineWidth; } // end for i up to numLines @@ -525,7 +535,7 @@ bool FileDisplay::edit(const FileDisplay* other) } if (bufContents < bufSize) - memset(data->buffer + bufContents, 0, bufSize - bufContents); + memset(data + bufContents, 0, bufSize - bufContents); short x = 0; short y = 0; @@ -572,7 +582,7 @@ bool FileDisplay::edit(const FileDisplay* other) short newByte = -1; if ((key == KEY_RETURN) && other && (other->bufContents > x + y*lineWidth)) { - newByte = other->data->line[y][x]; // Copy from other file + newByte = other->data[(y)*lineWidth+(x)]; // Copy from other file hiNib = ascii; // Always advance cursor to next byte } else if (ascii) { if (isprint(key)) newByte = (inputTable ? inputTable[key] : key); @@ -583,9 +593,9 @@ bool FileDisplay::edit(const FileDisplay* other) newByte = safeUC(key) - 'A' + 10; if (newByte >= 0) { if (hiNib) - newByte = (newByte * 0x10) | (0x0F & data->line[y][x]); + newByte = (newByte * 0x10) | (0x0F & data[(y)*lineWidth+(x)]); else - newByte |= 0xF0 & data->line[y][x]; + newByte |= 0xF0 & data[(y)*lineWidth+(x)]; } // end if valid digit entered } // end else hex if (newByte >= 0) { @@ -623,7 +633,7 @@ bool FileDisplay::edit(const FileDisplay* other) moveTo(offset); // Re-read buffer contents } else { SeekFile(file, offset); - WriteFile(file, data->buffer, bufContents); + WriteFile(file, data, bufContents); } } showPrompt(); @@ -650,11 +660,11 @@ void FileDisplay::setByte(short x, short y, Byte b) } // end if more than 1 byte past the end done: ++bufContents; - data->line[y][x] = b ^ 1; // Make sure it's different + data[(y)*lineWidth+(x)] = b ^ 1; // Make sure it's different } // end if past the end - if (data->line[y][x] != b) { - data->line[y][x] = b; + if (data[(y)*lineWidth+(x)] != b) { + data[(y)*lineWidth+(x)] = b; char str[3]; sprintf(str, "%02X", b); win.setAttribs(cFileEdit); @@ -700,7 +710,7 @@ void FileDisplay::moveTo(FPos newOffset) offset = 0; SeekFile(file, offset); - bufContents = ReadFile(file, data->buffer, bufSize); + bufContents = ReadFile(file, data, bufSize); } // end FileDisplay::moveTo //-------------------------------------------------------------------- @@ -835,7 +845,7 @@ bool FileDisplay::setFile(const char* aFileName) fileName[maxPath-1] = '\0'; win.put(0,0, fileName); - win.putAttribs(0,0, cFileName, screenWidth); + win.putAttribs(0,0, cFileName, paneWidth); win.update(); // FIXME bufContents = 0; @@ -846,24 +856,49 @@ bool FileDisplay::setFile(const char* aFileName) return false; offset = 0; - bufContents = ReadFile(file, data->buffer, bufSize); + bufContents = ReadFile(file, data, bufSize); return true; } // end FileDisplay::setFile //==================================================================== // Main Program: +//-------------------------------------------------------------------- +// Compute leftMar2/paneWidth/screenWidth from the current lineWidth: +// +// Layout of one pane's content, e.g. for lineWidth=16: +// "OOOO OOOO: HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH AAAAAAAAAAAAAAAA" +// <--10----> <---- hex: 3*lineWidth + groups ----------------> <-- ascii: lineWidth --> + +void computeLayoutMetrics() +{ + const int offsetFieldWidth = 10; // "OOOO OOOO:" + const int hexGroups = (lineWidth + 7) / 8; // extra space per 8-byte group + + leftMar2 = offsetFieldWidth + hexGroups + lineWidth * 3; + paneWidth = leftMar2 + lineWidth; + + screenWidth = (singleFile || !sideBySide) ? paneWidth + : (paneWidth * 2 + paneGap); +} // end computeLayoutMetrics + //-------------------------------------------------------------------- void calcScreenLayout(bool resize = true) { + computeLayoutMetrics(); + int screenX, screenY; ConWindow::getScreenSize(screenX, screenY); if (screenX < screenWidth) { ostringstream err; - err << "The screen must be at least " - << screenWidth << " characters wide."; + err << "The screen must be at least " << screenWidth + << " characters wide to show " << lineWidth + << " bytes/line" << ((!singleFile && sideBySide) ? " side by side" : "") + << ".\n" + << "Use -w/--width to show fewer bytes per line, or widen " + << "your terminal."; exitMsg(2, err.str().c_str()); } @@ -874,11 +909,14 @@ void calcScreenLayout(bool resize = true) exitMsg(2, err.str().c_str()); } - numLines = screenY - promptHeight - (singleFile ? 1 : 2); - - if (singleFile) + if (singleFile || sideBySide) { + // Single pane, or side-by-side panes sharing the same rows: + // only 1 row is needed for the filename header, not 2. + numLines = screenY - promptHeight - 1; linesBetween = 0; - else { + } else { + // Stacked (original) layout: two panes, one above the other. + numLines = screenY - promptHeight - 2; linesBetween = numLines % 2; numLines = (numLines - linesBetween) / 2; } @@ -1278,12 +1316,32 @@ int packHex(Byte* buf) void positionInWin(Command cmd, short width, const char* title) { inWin.resize(width, 3); - inWin.move((screenWidth-width)/2, - ((!singleFile && (cmd & cmgGotoBottom)) - ? ((cmd & cmgGotoTop) - ? numLines + linesBetween // Moving both - : numLines + numLines/2 + 1 + linesBetween) // Moving bottom - : numLines/2)); // Moving top + + if (sideBySide) { + const bool wantTop = singleFile || (cmd & cmgGotoTop); + const bool wantBottom = !singleFile && (cmd & cmgGotoBottom); + + short paneX, paneAvailWidth; + + if (wantTop && wantBottom) { + paneX = 0; paneAvailWidth = screenWidth; // Span both panes + } else if (wantBottom) { + paneX = paneWidth + paneGap; paneAvailWidth = paneWidth; // Right pane only + } else { + paneX = 0; paneAvailWidth = paneWidth; // Left pane only + } + + inWin.move(paneX + (paneAvailWidth-width)/2, numLines/2); + } else { + // Stacked (original) layout: position vertically over the top or + // bottom file's rows, horizontally centered across the full screen. + inWin.move((screenWidth-width)/2, + ((!singleFile && (cmd & cmgGotoBottom)) + ? ((cmd & cmgGotoTop) + ? numLines + linesBetween // Moving both + : numLines + numLines/2 + 1 + linesBetween) // Moving bottom + : numLines/2)); // Moving top + } inWin.border(); inWin.put((width-strlen(title))/2,0, title); @@ -1381,17 +1439,28 @@ bool initialize() inWin.hide(); int y; - if (singleFile) y = numLines + 1; - else y = numLines * 2 + linesBetween + 2; + if (singleFile || sideBySide) y = numLines + 1; + else y = numLines * 2 + linesBetween + 2; promptWin.init(0,y, screenWidth,promptHeight, cBackground); showPrompt(); if (!singleFile) diffs.resize(); - file1.init(0, (singleFile ? NULL : &diffs)); + file1.init(0, 0, (singleFile ? NULL : &diffs)); - if (!singleFile) file2.init(numLines + linesBetween + 1, &diffs); + if (!singleFile) { + if (sideBySide) { + file2.init(paneWidth + paneGap, 0, &diffs); + + // Draw a divider between the two side-by-side panes: + dividerWin.init(paneWidth, 0, paneGap, numLines + 1, cDivider); + for (int row = 0; row <= numLines; ++row) + dividerWin.putChar(paneGap/2, row, '|', 1); + dividerWin.update(); + } else + file2.init(0, numLines + linesBetween + 1, &diffs); + } return true; } // end initialize @@ -1769,7 +1838,10 @@ If FILE2 is omitted, just display FILE1.\n\ Options:\n\ --help display this help information and exit\n\ -L, --license display license & warranty information and exit\n\ - -V, --version display version information and exit\n"; + -V, --version display version information and exit\n\ + -s, --side-by-side show files side by side instead of stacked\n\ + -w, --width=N bytes per line in side-by-side mode (default 16)\n\ + -c, --config=FILE load color scheme from FILE\n"; } exit(exitStatus); @@ -1782,6 +1854,15 @@ bool usage(GetOpt* getopt, const GetOpt::Option* option, return false; // Never happens } // end usage +//-------------------------------------------------------------------- +bool setSideBySide(GetOpt*, const GetOpt::Option*, const char*, + GetOpt::Connection, const char*, int*) +{ + sideBySide = true; + return false; // This option never takes an argument; returning false + // tells GetOpt not to consume the next positional arg. +} // end setSideBySide + //-------------------------------------------------------------------- // Handle options: // @@ -1800,6 +1881,11 @@ void processOptions(int& argc, char**& argv) { '?', "help", NULL, 0, &usage }, { 'L', "license", NULL, 0, &license }, { 'V', "version", NULL, 0, &usage }, + { 's', "side-by-side", NULL, 0, &setSideBySide }, + { 'w', "width", NULL, GetOpt::needArg, &GetOpt::isLong, + &optLineWidth }, + { 'c', "config", NULL, GetOpt::needArg, &GetOpt::isString, + &optConfigPath }, { 0 } }; @@ -1814,6 +1900,13 @@ void processOptions(int& argc, char**& argv) argc -= --argi; // Reduce argc by number of arguments used argv += argi; // And adjust argv[1] to the next argument } + + if (optLineWidth < 4 || optLineWidth > 64) { + cerr << program_name + << ": --width must be between 4 and 64 bytes per line\n"; + exit(1); + } + lineWidth = int(optLineWidth); } // end processOptions //==================================================================== @@ -1837,6 +1930,23 @@ VBinDiff " PACKAGE_VERSION ", Copyright 1995-2017 Christopher J. Madsen\n\ VBinDiff comes with ABSOLUTELY NO WARRANTY; for details type `vbindiff -L'.\n"; singleFile = (argc == 2); + + { + string configErr; + if (optConfigPath) + configErr = ConWindow::loadColorConfig(optConfigPath, false); + else { + const char* home = getenv("HOME"); + if (home) + configErr = ConWindow::loadColorConfig( + (string(home) + "/.vbindiffrc").c_str(), true); + } + if (!configErr.empty()) { + cerr << program_name << ": " << configErr << '\n'; + return 1; + } + } + if (!initialize()) { cerr << '\n' << program_name << ": Unable to initialize windows\n"; return 1; @@ -1870,6 +1980,7 @@ VBinDiff comes with ABSOLUTELY NO WARRANTY; for details type `vbindiff -L'.\n"; file1.shutDown(); file2.shutDown(); inWin.close(); + dividerWin.close(); promptWin.close(); ConWindow::shutdown(); From 0312dcea4e2b083fb7a5e188b46b304709587f12 Mon Sep 17 00:00:00 2001 From: John Feehley Date: Wed, 1 Jul 2026 21:25:40 -0400 Subject: [PATCH 2/2] Add modern color theme configuration for VBinDiff This file contains the modern color theme configuration for VBinDiff, including styles for background, prompts, and file diffs. --- vbindiffrc-modern.conf | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 vbindiffrc-modern.conf diff --git a/vbindiffrc-modern.conf b/vbindiffrc-modern.conf new file mode 100644 index 0000000..6924284 --- /dev/null +++ b/vbindiffrc-modern.conf @@ -0,0 +1,25 @@ +# VBinDiff "modern" color theme. +# +# This reproduces the dark theme that was briefly the built-in default: +# black background, bold cyan filename/mode bars, bold red diffs, bold +# yellow edits, bold blue divider between side-by-side panes. +# +# To use it every time, copy this file to ~/.vbindiffrc: +# cp vbindiff-modern.conf ~/.vbindiffrc +# +# Or load it explicitly for one run: +# vbindiff -c vbindiff-modern.conf -s file1 file2 +# +# Format: styleName = foreground/background[/bold|reverse|underline] +# Colors: black, red, green, yellow, blue, magenta, cyan, white + +background = white/black +promptWin = white/black +promptKey = cyan/black/bold +promptBdr = white/black/bold +currentMode = black/cyan/bold +fileName = black/cyan/bold +fileWin = white/black +fileDiff = red/black/bold +fileEdit = yellow/black/bold +divider = blue/black/bold