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 or italic 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 supports one top-level table:
[color]. Set color.depth to 16, 256 or 24 to choose the preferred
output; it defaults to truecolour (24). The palettes live in
[color.ansi16], [color.ansi256] and [color.truecolor]. Unknown tables,
styles and fields are reported as errors.
The two palette tables use the same style names and fields:
| Field | Value | Meaning |
|---|---|---|
color |
Palette-specific colour | Foreground colour |
bgcolor |
Palette-specific colour | Background colour; diff styles only |
bold |
true or false |
Enable or disable bold text |
italic |
true or false |
Enable or disable italic text |
Inline tables keep short styles compact:
[color]
depth = 256
[color.ansi16]
add = { color = "green", bold = true }
[color.ansi256]
add = { color = 114, bold = true }
add_highlight = { color = 231, bgcolor = 22 }
[color.truecolor]
add = { color = "#87d787", bold = true }
add_highlight = { color = "#ffffff", bgcolor = "#005f00" }Alternatively you may use the longer, equivalent, TOML table form:
[color.ansi256.add_highlight]
color = 231
bgcolor = 22
bold = true
italic = trueAll styles and fields are optional. Missing ANSI16 values use Jiff's built-in
defaults. Missing ANSI256 values inherit the corresponding resolved ANSI16
values. Missing truecolour values inherit ANSI256 using the standard xterm
RGB mapping. Use "default" to select the terminal's normal foreground or
background explicitly:
[color.ansi16]
add = { color = "default" }
[color.ansi256]
add = { color = "default" }
[color.truecolor]
add = { color = "default" }The old layout, where styles appeared directly below [color], is no longer
accepted.
Jiff uses the preferred palette when the terminal supports it. Truecolour
falls back to ANSI256 and then ANSI16; ANSI256 falls back to ANSI16. An
explicit depth of 16 always uses ANSI16, and an explicit depth of 256 still
emits indexed colours on a truecolour terminal. --no-color continues to
disable all palettes.
These styles control the diff itself:
| Style | Used for | Default foreground | Default background |
|---|---|---|---|
same |
Unchanged text | Terminal default | Terminal default |
line_number |
Side-by-side line-number gutters | Terminal default | Terminal default |
line_number_add |
Line numbers beside additions | Inherits line_number |
Inherits line_number |
line_number_remove |
Line numbers beside removals | Inherits line_number |
Inherits line_number |
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 ten accept color, bgcolor, bold and italic. Their built-in text
attributes are both false. line_number, line_number_add and
line_number_remove apply to the complete padded line-number cell, but not
the vertical rule beside it, so a background colour fills the number cleanly
without catching the divider. The two changed-line styles inherit
line_number when omitted. The +/- markers inherit the corresponding diff
colour and italics, and are deliberately bold. 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, bold and italic; both text attributes
default to 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.
ANSI16 foregrounds and backgrounds use these case-insensitive names:
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).
ANSI256 foregrounds and backgrounds use integer indexes from 0 to 255, or
the string "default".
Truecolour foregrounds and backgrounds use a six-digit RGB value such as
"#89b4fa", or the string "default". Short hex values, alpha channels and
CSS colour names are not accepted.
The repository includes complete themes in
jiff-configure/themes. High Contrast Light, High
Contrast Dark, Tokyo Night and Twilight Dark are Jiff themes. The other 25 use
the static themes bundled with bat: 1337, the four Catppuccin variants,
Coldark, DarkNeon, Dracula, GitHub, Gruvbox, Monokai Extended, Nord, OneHalf,
Solarized, Sublime Snazzy, TwoDark, Chalkboard and Zenburn.
Each theme has a truecolour palette plus indexed and named fallbacks. The
syntax styles come directly from its bat theme. Jiff derives the diff
backgrounds from that palette, because bat does not define diff colours.
Terminals can customise their first 16 colours, so the ANSI16 fallback's exact
appearance still depends on the terminal theme. bat's ansi and Base16
templates are not included: they rely on a terminal-specific palette rather
than defining fixed colours.
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. If Jiff already has a configuration file, the tool loads it as
Current configuration using the same path lookup as Jiff. To preview a pair
of your own files:
uv run jiff-configure OLD NEWUse the picker to switch between the current configuration, the default colours
and the packaged themes. Preferred output chooses the depth used by Jiff and
the previews; Palette to edit switches between the indexed palette and its
named fallbacks. ANSI16 fields use named selectors. ANSI256 fields open a 16 by
16 colour grid which works with the mouse or arrow keys. Truecolour fields use
a validated #RRGGBB editor and colour swatch. Both pickers include a separate
terminal-default choice.
The Side-by-side, Inline, and Three-way tabs use Jiff's real Python renderer, so they update as each palette changes. If the current terminal cannot display the preferred depth, the previews use the best fallback and say so above the tabs. The saved TOML contains all three complete palettes, including separate bold and italic settings.
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. Side-by-side Git diffs put each path above its pane when both fit.
Added and deleted files use /dev/null for the missing side. Wider paths and
inline output keep the Git-style --- and +++ headings.
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 positional
protocol. Renames and copies include the new path as an extra argument.
--git-external-diff tells Jiff to parse those arguments, retain both paths,
keep 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~Added and deleted files use /dev/null for the missing side.
Unresolved files are another difference between the two Git interfaces.
git diff calls Jiff with the unresolved path, which lets Jiff read all three
stages from Git's index. git difftool does not call a custom file-mode tool
for unresolved paths. Use the diff.external integration above when you want
Jiff's three-way conflict view during a merge or rebase.
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