A terminal diff tool supporting sub-line diffs and side-by-side output display
This repo has two separate, fully featured implementations: one written in Rust, the other in Python. The Rust version is expected to be faster, but the Python version is more portable. The core diff behaviour is identical, though syntax highlighting may differ slightly because the implementations use different language engines.
Install the Rust implementation with Cargo:
cargo install --path .Or install the Python implementation as a standalone uv tool:
uv tool install ./pythonBoth commands install a jiff executable. Only install one implementation at a
time unless you deliberately arrange their order in $PATH.
Invoke Jiff with two files or directories to see the difference between them:
jiff FILE1 FILE2
jiff DIR1 DIR2By default, Jiff renders diffs in side-by-side mode. To render diffs in the more
conventional inline mode, pass --inline.
Long output from either implementation is sent to $PAGER, using less by
default. Output which fits in the terminal, or is redirected to another
command, is printed directly. Pass --no-pager to always print directly.
By default Jiff shows every unchanged line. Pass -U<n> or --unified=<n> to
show at most <n> lines of context on either side of each change. For example,
this shows three context lines:
jiff -U3 FILE1 FILE2Omitted regions are marked with their number of unchanged lines in either
output layout. -U0 shows only changed lines and those markers.
Jiff also supports three-way merge diffs. Pass three files to compare two versions against a common base. The second file is the base, so the order is:
jiff LOCAL BASE REMOTESide-by-side mode draws one pane per file. Changes are highlighted in the two
outer panes, relative to base, while the central (base) pane highlights the
text changed by the local side, the remote side or both. --inline falls back
to two labelled diffs, LOCAL against BASE followed by BASE against
REMOTE.
Jiff automatically detects source languages from the input filenames. Git
difftool comparisons use the repository path supplied through --path, rather
than trying to identify Git's temporary filenames.
Use --syntax=LANGUAGE to override automatic detection:
jiff --syntax=python FILE1 FILE2Common language names and extensions are accepted. The Python implementation uses Pygments, while Rust uses syntect, so their complete language lists and a few token boundaries differ. An unknown automatically detected language falls back to plain text. An unknown explicit language is reported as an error.
Pass --no-syntax to retain Jiff's diff colours without token highlighting.
--no-color disables both. The built-in syntax palette is deliberately muted:
- comments are grey;
- keywords are magenta;
- strings are cyan;
- numbers are blue;
- function and type names are yellow.
Syntax highlighting only changes foreground colour and optional bold text - diff highlights control the background colour.
Jiff supports an optional XDG-style config file for persisting configuration.
Jiff uses the first configuration file it finds in this order:
- The path in
$JIFF_CONFIG, when set. If no other paths are searched. $XDG_CONFIG_HOME/jiff/config.toml, or~/.config/jiff/config.tomlwhen$XDG_CONFIG_HOMEis not set.~/.jiffconfig.
The XDG path is recommended for normal use. Create it with:
mkdir -p ~/.config/jiff
touch ~/.config/jiff/config.tomlJIFF_CONFIG is useful for trying another theme temporarily:
JIFF_CONFIG=jiff-configure/themes/high-contrast-light.toml jiff OLD NEWThe config file must contain valid TOML and currently supports one top-level
table: [color]. Unknown tables, styles and fields are reported as errors.
Each entry below [color] names a style. A style has up to three fields:
| Field | Value | Meaning |
|---|---|---|
color |
Colour name | Foreground colour |
bgcolor |
Colour name | Background colour; diff styles only |
bold |
true or false |
Enable or disable bold text |
Inline tables keep short styles compact:
[color]
add = { color = "blue", bold = true }
add_highlight = { color = "yellow", bgcolor = "blue" }Alternatively you may use the longer, equivalent, TOML table form:
[color.add_highlight]
color = "yellow"
bgcolor = "blue"
bold = trueAll styles and fields are optional. Omitted values keep their built-in default.
Use the colour name default when you want to clear a built-in foreground or
background instead:
[color]
add = { color = "default" }That example makes added text use the terminal's normal foreground colour.
These styles control the diff itself:
| Style | Used for | Default foreground | Default background |
|---|---|---|---|
same |
Unchanged text | Terminal default | Terminal default |
omitted |
... N unchanged lines ... markers |
bright_black |
Terminal default |
add |
Normal added text and unchanged characters in paired lines | green |
Terminal default |
add_highlight |
Changed characters and unpaired side-by-side additions | black |
green |
remove |
Normal removed text and unchanged characters in paired lines | red |
Terminal default |
remove_highlight |
Changed characters and unpaired side-by-side removals | black |
red |
overlap_highlight |
Middle-pane characters changed by both outer files in a three-way | black |
yellow |
All seven accept color, bgcolor and bold. Their built-in bold value is
false. Line numbers and the +/- markers inherit the corresponding diff
colour and are deliberately bold so they remain visible beside highlighted
text. overlap_highlight is only used in three-way side-by-side output.
Syntax configuration only changes how token categories are drawn.
| Style | Used for | Default foreground |
|---|---|---|
syntax_comment |
Comments and documentation | bright_black |
syntax_comment_highlight |
Comments within highlighted text | bright_black |
syntax_keyword |
Language keywords | magenta |
syntax_keyword_highlight |
Language keywords within highlighted text | magenta |
syntax_string |
String literals | cyan |
syntax_string_highlight |
String literals within highlighted text | cyan |
syntax_number |
Numeric literals | blue |
syntax_number_highlight |
Numeric literals within highlighted text | blue |
syntax_definition |
Function, type and other definition names | yellow |
syntax_definition_highlight |
Function, type and other definition names within highlighted text | yellow |
These ten styles accept color and bold; their built-in bold value is
false. They do not accept bgcolor. Diff backgrounds must remain in control,
and syntax highlighting renders on top of diff highlights.
Use --no-syntax to ignore the syntax styles while retaining the diff colours.
Use --no-color to disable both diff and syntax styling.
The following case-insensitive colour names are supported:
default
black
bright_black
red
bright_red
green
bright_green
yellow
bright_yellow
blue
bright_blue
magenta
bright_magenta
cyan
bright_cyan
white
bright_white
gray and grey are aliases for bright_black; purple is an alias for
magenta. default means the terminal's normal foreground or background (not
Jiff's built-in value). Hex colours, RGB values, ANSI colour numbers and colours
outside the standard 16-colour ANSI palette are not currently supported.
The repository includes seven complete themes which you can use as-is or extend:
| Theme | Best suited to | Character |
|---|---|---|
| High contrast light | Light terminals | Crisp blue and magenta diff colours |
| High contrast dark | Dark terminals | Bright cyan and yellow diff colours |
| Catppuccin Mocha | Dark terminals | Soft green, magenta and cyan |
| Dracula | Dark terminals | Green and red diffs with purple syntax |
| Gruvbox Dark | Dark terminals | Warm, bright foregrounds on restrained backgrounds |
| Nord | Dark terminals | Cool cyan, red and blue |
| Tokyo Night | Dark terminals | Cyan and magenta with blue syntax |
The first two prioritise contrast and colour-blind accessibility. The other five borrow the colour relationships of popular editor themes and are intended to render well in these themes (though bear in mind not all terminals implement their themes in the same way). Jiff's config uses ANSI colour names, so your terminal theme will choose the exact shades. This generally makes the palettes sit naturally alongside a matching terminal theme, but they are not exact RGB reproductions.
Copy any theme to the standard XDG location to use it:
mkdir -p ~/.config/jiff
cp jiff-configure/themes/high-contrast-dark.toml ~/.config/jiff/config.tomlYou can build a theme interactively using the separate jiff-configure tool.
Run it from the repository with:
uv run jiff-configureThis starts with a small built-in Python diff which exercises all supported diff functionality. To preview a pair of your own files:
uv run jiff-configure OLD NEWPick one of the packaged themes as a starting point, then adjust the settings for each style. The Side-by-side, Inline, and Three-way tabs use Jiff's real Python renderer, so they update as the palette changes.
Press Ctrl+S or use the Save button to save your theme.
Once jiff is installed and available in $PATH, configure it as a custom Git
difftool with:
git config --global diff.tool jiff
git config --global difftool.jiff.cmd 'jiff --path "$MERGED" "$LOCAL" "$REMOTE"'
git config --global difftool.prompt false
git config --global difftool.trustExitCode trueRemove --global if the configuration should only apply to the current
repository.
For a one-off comparison without changing your Git configuration, use
--extcmd:
git difftool --no-prompt --extcmd='jiff --no-pager --path "$BASE"'The --no-pager in this example avoids opening a pager for each changed file.
For a multi-file comparison with automatic paging, use the --dir-diff form
instead. Jiff returns zero after displaying a text or binary comparison and
non-zero when it cannot read, configure or display the diff.
difftool.trustExitCode makes Git report those failures rather than silently
continuing.
Git's ordinary diff command uses a different interface from git difftool.
It calls an external diff once per changed path using its own seven-argument
protocol. --git-external-diff tells Jiff to parse those arguments, retain its
colours and leave Git in charge of the pager.
Configure Jiff globally, or omit --global to use it in one repository:
git config --global diff.external 'jiff --git-external-diff'Ordinary diff commands will now use Jiff:
git diff
git diff --cached
git diff HEAD~git show and git log do not enable external diff programs by default. Pass
--ext-diff, or add shorter aliases:
git show --ext-diff HEAD
git log -p --ext-diff
git config --global alias.jshow 'show --ext-diff'
git config --global alias.jlog 'log -p --ext-diff'The aliases are then available as git jshow and git jlog. Git aliases
cannot replace built-in commands, so a Git configuration cannot make the exact
command git show imply --ext-diff.
External diff output is intended for people to read; it is not a patch. Use
git diff --no-ext-diff for scripts or anything which needs Git's normal patch
format.
Jiff automatically renders merge conflicts using the same three-way diff functionality described above. This deliberately represents the index, not the working-tree file. Any conflict markers or edits made since the merge are therefore not included. Some Git commands render unresolved paths with Git's built-in combined diff instead of calling an external helper; Jiff cannot replace output when it is not invoked.
cargo buildcargo run -- <options>Unit tests:
cargo testSystem tests:
uv run robot testsOptionally, the Python tests can be skipped with:
uv run robot -v SKIP_PYTHON_TESTS:True testsuv run jiff FILE1 FILE2Unit tests:
uv run python -m unittest discover -s python/tests
uv run python -m unittest discover -s jiff-configure/testsSystem tests:
uv run robot testsOptionally, the Rust tests can be skipped with:
uv run robot -v SKIP_RUST_TESTS:True testsCreate the development environment and install the git hooks with:
uv sync
uv run pre-commit installOnce the hooks are installed they will run automatically on commit. You can run pre-commit manually with:
uv run pre-commit run --all-files