A faithful terminal port of GNOME Meld: the same diff engine (vendored verbatim), the same colors, the same keybindings — over plain SSH. Two- and three-way file comparison and merging, folder comparison, and a version-control view.
tmeld a.py b.py # 2-way compare/edit
tmeld local.py base.py remote.py # 3-way merge (middle = merged file)
tmeld dirA dirB # folder comparison (Enter opens files)
tmeld . # version-control view (git, hg, svn, ...)
tmeld a b --diff c d --diff x y # extra comparison tabs (like meld --diff)
tmeld --theme meld-dark a b # Meld's dark scheme
tmeld --show-line-numbers a b # line numbers (off by default, as in Meld)
tmeld --web a b # serve it to a browser instead (see bmeld)
Line numbers are hidden by default, matching Meld's defaults; the status bar
always shows Ln N, Col N for the focused pane. --show-line-numbers brings
them back (in bmeld too).
From PyPI: pipx install tmeld (or pip install tmeld).
As a Debian package: grab tmeld_*_all.deb from the GitHub release, or
build one yourself:
sudo apt install build-essential debhelper dpkg-dev \
python3 python3-pip python3-venv git
git clone https://github.com/mitnits/tmeld.git
cd tmeld && maint/mkdeb.sh # writes ../tmeld_<ver>+<date>.<sha>_all.deb
sudo apt install ../tmeld_*_all.deb # apt, so Recommends come tooBuilding needs network: it fetches the pinned Textual from PyPI and bundles
it under /usr/share/tmeld/lib, because tmeld needs a newer Textual than
Debian ships. System Python packages are untouched. The result is
Architecture: all, so one build serves every machine — copy the .deb
around and apt install ./… it.
Use apt install ./file.deb rather than dpkg -i, so the recommended
python3-aiohttp is pulled in. That is what bmeld needs, and it is
deliberately not bundled: its C extensions would make the package
architecture-dependent.
maint/mkdeb.sh stamps each build <version>+<date>.<sha>, so a rebuild
installs over the previous one instead of being refused as the same version
(0.4.0 < 0.4.0+20260708.f683ae7 < 0.5.0).
| Action | Keys |
|---|---|
| Next / previous change | Alt+Down, Ctrl+D / Alt+Up, Ctrl+E |
| Push chunk left / right | Alt+Left / Alt+Right |
| Pull chunk from left / right | Alt+Shift+Right / Alt+Shift+Left |
| Copy chunk above / below | Alt+[ Alt+] / Alt+; Alt+' |
| Delete chunk | Alt+Delete |
| Next / previous conflict (3-way) | Ctrl+K / Ctrl+J |
| Merge all non-conflicting (3-way) | Alt+M |
| Undo / cut / copy | Ctrl+Z, Alt+Z / Ctrl+X, Alt+X / Ctrl+C, Alt+C |
| Save | Ctrl+S |
| Next / previous pane | Alt+PgDn / Alt+PgUp |
| Close tab / quit on the last one | Ctrl+W (twice if unsaved), or the ✕ |
| Next / previous tab | Ctrl+Alt+PgDn / Ctrl+Alt+PgUp |
| Quit | Esc, Ctrl+Q (Esc warns once if unsaved) |
Gutter arrows between panes are clickable (they push the chunk); the right-edge map is click-to-jump. On macOS terminals, set "Option as Esc+" (iTerm2: Profiles → Keys) so Alt bindings arrive.
In folder comparisons: Enter compares the file under the cursor (or expands/collapses a folder), Alt+Left/Right copy the row to that neighbor pane, Delete deletes it (press twice to confirm), Alt+Down/Up jump between differing rows, and Alt+PgDn/PgUp move the focused pane (the column copy/delete act on). Meld's default filename filters (backups, VCS metadata, binaries, OS cruft) apply.
In the version-control view (tmeld . anywhere in a working copy):
Enter compares a changed file against the repository (repo side
read-only); a conflicted file opens as a remote/merge/local 3-way whose
middle-pane saves resolve the working file. c commits (Meld's Ctrl+M
IS Enter in a terminal, so the mnemonic moved), r reverts, Delete
deletes, Ctrl+R/F5 rescans.
pip install 'tmeld[web]' adds the bmeld command — the same program as
tmeld --web, under its own name. --port implies --web, since a port
means nothing in a terminal:
tmeld a b # terminal
tmeld --web a b # browser, random port
tmeld --port 8731 a b # browser on a fixed port
bmeld a b # exactly `tmeld --web a b`
Options that belong to the other front-end are refused rather than ignored
(tmeld --bind 0.0.0.0 says --bind requires --web).
It starts a local
server (127.0.0.1 only, token-protected) and prints a clickable URL —
the same engine, palette and keybindings, rendered by CodeMirror with
real SVG linkmap curves. Typing is local and instant; the server
rediffs debounced snapshots and is the only thing that writes files.
The process exits with the same mergetool contract as tmeld, so the
.gitconfig stanza below works with cmd = bmeld ... too.
bmeld local.py base.py remote.py # prints http://127.0.0.1:PORT/t/TOKEN
bmeld dirA dirB # folder comparison (Enter opens files)
bmeld . # version-control view (commit/revert)
bmeld a b --diff c d # extra comparison tabs
bmeld --port 8731 a.py b.py # fixed port: add a LocalForward line
# to ~/.ssh/config and remote links
# open on your local browser
bmeld --bind 0.0.0.0 a.py b.py # reachable from other machines (see below)
File, folder, and version-control comparisons open as tabs (with ✕ close buttons); Enter/double-click a tree row opens a file comparison.
Over SSH, bmeld prints the port-forward one-liner instead of trying to
open a browser. Sessions survive reloads; closing the tab without
saving a merge exits 1 after a grace period (--grace).
By default bmeld listens on loopback only. --bind 0.0.0.0 accepts
connections from other machines and prints a URL with this host's
outbound address (override with --advertise HOST). Understand what
that costs: the unguessable token in the URL becomes the only thing
between the network and a process that reads and writes the files under
comparison, and it travels in cleartext over HTTP. Use it on a network
you trust, or keep the SSH tunnel.
On terminals with graphics support, the gutter between panes widens
and Meld's anti-aliased connector curves are drawn there as real
pixels — kitty graphics protocol (kitty, WezTerm, Ghostty) or sixel
(iTerm2, recent VTE), auto-detected at startup. --graphics none|sixel|kitty overrides the probe. Everything else stays
cell-based; without graphics you keep the compact 3-column gutter.
tmeld follows Meld's convention: tmeld $LOCAL $MERGED $REMOTE, the
middle pane is the merged file. The exit code is 0 only if the middle
pane was saved, so git can trust it:
[merge]
tool = tmeld
[mergetool "tmeld"]
cmd = tmeld "$LOCAL" "$MERGED" "$REMOTE"
trustExitCode = truetmeld -o OUTPUT local base remote redirects middle-pane saves to
OUTPUT (like meld -o) if you'd rather keep the base file untouched.
For diffs: git difftool -x tmeld or
[diff]
tool = tmeld
[difftool "tmeld"]
cmd = tmeld "$LOCAL" "$REMOTE"GPL-2.0-or-later (see LICENSE), like Meld — whose engine this project
vendors byte-for-byte apart from mechanical import rewrites
(tmeld/_vendor/meld/, pinned commit recorded in
tmeld/_vendor/UPSTREAM, rewrites applied by maint/vendor.py). The
vendored vc/ package is BSD 2-clause (its COPYING ships alongside).
tmeld is an independent project, not affiliated with or endorsed by the Meld or GNOME projects. All credit for the diff engine and the design this port imitates goes to Meld and its maintainers.