Beautiful Diff view widget for Textual applications
Project description
Textual Diff View
Textual Diff View is a Textual widget to display beautiful diffs in your terminal application. Originally built for Toad, this widget may be used standalone.
This is what it can look like...
Features
The DiffView widget displays two versions of a file with syntax and changes clearly highlighted.
Deleted lines / characters are shown with a red highlight.
Added lines / characters are shown with a green highlight.
There are two layout options; a unified view which shows the two files top-to-bottom with highlights, and a split view which shows the two files next to each other.
DiffView can also display annotations ("+" and "-" for added and deleted), to improve readability for color blind users.
Textual's theming system provides a variety of themes for the diff view, both light and dark.
Example
The following is a simple app to display a diff between two files from the command line.
from textual.app import App, ComposeResult
from textual import containers
from textual.reactive import var
from textual import widgets
from textual_diff_view import DiffView, LoadError
class DiffApp(App):
"""Simple app to display a diff between two files."""
BINDINGS = [
("space", "toggle('split')", "Toggle split"),
("a", "toggle('annotations')", "Toggle annotations"),
]
split = var(True)
annotations = var(True)
def __init__(self, original: str, modified: str) -> None:
self.original = original
self.modified = modified
super().__init__()
def compose(self) -> ComposeResult:
yield containers.VerticalScroll(id="diff-container")
yield widgets.Footer()
async def on_mount(self) -> None:
try:
diff_view = await DiffView.load(self.original, self.modified)
except LoadError as error:
self.notify(str(error), title="Failed to load code", severity="error")
else:
diff_view.data_bind(DiffApp.split, DiffApp.annotations)
await self.query_one("#diff-container").mount(diff_view)
if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
print("Usage python tdiff.py PATH1 PATH2\nTry: python tdiff.py")
else:
app = DiffApp(sys.argv[1], sys.argv[2])
app.run()
You can find this file in the examples/ directory.
Run it with the following:
uv run python tdiff.py example1.rs example2.rs
Use space to toggle unified / split, and a to toggle annotations.
Screenshots
A few screenshots taken from the example app:
|
|
|
|
|
|
Installing
Texual Diff View is on PyPI and may be installed with pip, or uv.
Here's how to install with uv:
uv add textual-diff-view
How to use
Import the widget with the following:
from textual_diff_view import DiffView
Then yield an instance of DiffView in your compose method.
The constructor accepts 4 positional arguments:
| Argument | type | Purpose |
|---|---|---|
| path_original | str |
A path to the original code |
| path_modified | str |
A path to the modified code |
| code_original | str |
The contents of the original code |
| code_modified | str |
The contents of the modified code |
Additionally, the constructor accepts the standard keyword arguments, name, id, and classes——which have the same meaning as Textual's built in widgets.
Here's a very simple example:
from textual.app import App, ComposeResult
from textual import containers
from textual_diff_view import DiffView
HELLO1 = """
def greet():
print "Hello!"
greet()
"""
HELLO2 = """
def greet(name:str):
print(f"Hello, {name}!")
greet('Will')
"""
class Hello(App):
def compose(self) -> ComposeResult:
with containers.VerticalScroll():
yield DiffView("hello1.py", "hello2.py", HELLO1, HELLO2)
Hello().run()
Note that we put the DiffView within a VerticalScroll, so the user may scroll the container if the diff doesn't fit.
The above code will generate the following output:
Load constructor
DiffView provides an alternative constructor, DiffView.load, which also loads the code.
If both your original code and modified code is on disk, this may be simpler than the standard constructor.
DiffView.load accepts the following positional arguments:
| Argument | Type | Purpose |
|---|---|---|
| path_original | str or Path |
A path to the original code |
| path_modified | str or Path |
A path to the modified code |
Since load is a coroutine, you would typically call it from a message handler in another widget, or App, then mount it somewhere in the DOM.
The code would look something like the following:
diff_view = await DiffView.load("original.py", "modified.py")
await self.query_one("VerticalScroll").mount(diff_view)
Reactives
The DiffView supports the following reactive attributes.
| Name | Type | Explanation |
|---|---|---|
split |
bool |
Enables split view when True, or unified view when False |
auto_split |
bool |
Automatically enable split view if there is enough space to fit the longest lines from both file. |
annotations |
bool |
Enable annotations ("+" or "-" symbols). It is reccomended that apps always offer this for color blind users. |
Roadmap
There are a few remaining features that I anticipate a need for:
- Word wrapping The widget currently supports horizontal scrolling (via mouse-wheel, trackpad, or shift+mouse-wheel). This works rather well, but has the downside that it is not especially discoverable. An option to enable word wrapping would be useful.
- ANSI theme A future version will add support for ANSI themes, which is limited to the user's choice of 16 colors. It will never look as good, but some people say they prefer it.
There are also a few more high-effort features that I could be tempted to implement:
- Swappable diff methods. There is no perfect diff algorithm. They all have their trade-offs.
The
DiffViewwidget uses Python'sdifflibbut it could offer an interface to add other diff algorithms. - AST level diffs A diff view that works at the AST level can offer diffs that more closely reflect how a human might edit code.
License
DiffView is licensed under the terms of the AGPL license. A commercial license is available if you aren't comfortable with the copyleft restriction. Contact Will McGugan for more information.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file textual_diff_view-0.1.2.tar.gz.
File metadata
- Download URL: textual_diff_view-0.1.2.tar.gz
- Upload date:
- Size: 9.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7da4d90aba8c800be03c67fa42aa99627000b12ef787847cd3eb9115d675c423
|
|
| MD5 |
9155e026d97700fc96bc260ab84407cd
|
|
| BLAKE2b-256 |
0298fd90601427bae1a56696051c46e2b474c86774eda8239c2957dc4825282b
|
File details
Details for the file textual_diff_view-0.1.2-py3-none-any.whl.
File metadata
- Download URL: textual_diff_view-0.1.2-py3-none-any.whl
- Upload date:
- Size: 10.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d71ab87ba78cb0e64680840fe3e840d4faf66801d785d3060f1cda8c468493be
|
|
| MD5 |
4c35d499dd20d6c36fe2ddcd6d28a2e2
|
|
| BLAKE2b-256 |
865ba69f714d200fc41a2070f37c1d2708432bbb2e4a6afe43c3daddacd8d7df
|