oyo
A diff viewer that works two ways: step through changes or review a classic scrollable diff.
https://github.com/user-attachments/assets/0f43b54b-69fe-4cf3-9221-a7749872342b
oyo extends traditional diffs with an optional step-through mode. Use it like a normal diff viewer with scrolling and hunk navigation, or step through changes one at a time and watch the code evolve. You can switch between both modes at any time.
Two ways to use oyo
1. Classic diff (scroll-only)
Review all changes at once, scroll freely, and jump between hunks, just like a traditional diff viewer.
- Scroll the full diff
- Jump between hunks
- No stepping required
Enable with:
oy --no-step- Toggle in-TUI with
s - Set
stepping = falsein config
2. Step-through review (default)
Apply changes incrementally and watch the file transform from old to new.
- Step change-by-change
- See precise evolution of the code
- Useful for large refactors or careful reviews
oyo does not replace classic diffs, it adds a new way to review them.
Features
- Classic diff mode (no-step) Scroll the full diff with hunk navigation, no stepping required
- Step-through navigation Move through changes one at a time with keyboard shortcuts
- Hunk navigation Jump between groups of related changes in both modes
- Four view modes:
- Unified: Watch the code morph from old to new state
- Split: See old and new versions with synchronized stepping
- Evolution: Watch the file evolve, deletions simply disappear
- Blame: Per-line git blame gutter (opt-in)
- Inline review comments: Add/update/remove line and hunk comments across views; printed to stdout on quit
- Word-level diffing: See exactly which words changed within a line
- Multi-file support: Navigate between changed files with preserved positions
- Search: Regex search with to jump between matches
- Syntax highlighting: Toggle on/off for code-aware coloring (auto-enabled in no-step mode)
- Blame hints: One-shot or toggle blame previews while stepping (opt-in)
- Command palette: Search for commands and files without leaving the diff
- Line wrap: Toggle wrapping for long lines
- Fold unchanged blocks: Toggle to collapse long context sections
- Animated transitions: Smooth fade in/out animations as changes are applied
- Playback: Automatically step through all changes at a configurable speed
- Git integration: Works as a git external diff tool or standalone
- Commit picker: Browse recent commits and pick ranges interactively (
oy view) - Themes: Built-in themes plus
.tmThemesyntax themes (configurable, with light/dark variants) - Configurable: XDG config file support for customization
Installation
npm (macOS, Linux)
Pi package (/diff, /review commands)
Homebrew (macOS, Linux)
AUR (Arch Linux)
Cargo
Usage
Classic diff (scroll-only)
# or toggle in-app with `s`
Step-through diff
Compare files
Compare a file against HEAD
Runs a working-tree vs HEAD diff for that file (like git diff path/to/file.rs).
Commit picker
View modes
Autoplay
Git ranges
Staged changes
Review output
# default: prints review comments to stdout on quit
# also write review comments to a file
# file-only output (for tool integrations)
# ephemeral review session (disable autosave/restore)
# start a fresh persisted review session (clear saved state for this diff)
Git Integration
Recommended (git difftool)
~/.gitconfig:
Note: keep your pager (
less,moar,moor) forgit diff. Do not setcore.pagerorinteractive.diffFiltertooy.
Jujutsu (jj)
[]
= "never"
= ["oy", "$left", "$right"]
[]
= ["oy", "$left", "$right"]
Keyboard Shortcuts
Vim-style counts: Most navigation commands support count prefixes (e.g., 10j moves 10 steps forward, 5J scrolls down 5 lines).
| Key | Action |
|---|---|
↓ / j |
Next step (scrolls in no-step mode; moves file selection when focused) |
↑ / k |
Previous step (scrolls in no-step mode; moves file selection when focused) |
→ / l |
Next hunk (scrolls in no-step mode) |
← / h |
Previous hunk (scrolls in no-step mode) |
b |
Jump to beginning of current hunk (scrolls in no-step mode) |
e |
Jump to end of current hunk (scrolls in no-step mode) |
gb |
Blame current step (opt-in, step mode) |
p / P |
Peek change (modified → old → mixed) / Peek old hunk |
y / Y |
Yank line/hunk to clipboard |
/ |
Search (diff pane, regex) |
n / N |
Next/previous match |
:line / :h<num> / :s<num> |
Go to line / hunk / step |
< |
First applied step |
> |
Last step |
gg |
Go to start (scroll-only in no-step mode) |
G |
Go to end (scroll-only in no-step mode) |
Space / B |
Autoplay forward/reverse |
Tab |
Cycle view mode |
Shift+Tab |
Cycle view mode (reverse) |
K |
Scroll up (supports count) |
J |
Scroll down (supports count) |
H |
Scroll left (supports count) |
L |
Scroll right (supports count) |
0 |
Start of line (horizontal) |
$ |
End of line (horizontal) |
Ctrl+u |
Half page up |
Ctrl+d |
Half page down |
Ctrl+g |
Show full file path |
gy / gY |
Copy patch for line/hunk |
Ctrl+p |
Command palette |
Ctrl+Shift+p |
Quick file search |
z |
Center on active change |
Z |
Toggle zen mode |
a |
Toggle animations |
w |
Toggle line wrap |
f |
Toggle context folding |
t |
Toggle syntax highlight |
E |
Toggle evo syntax (context/full) |
c / C |
Next/prev conflict |
m / M |
Add/update line/hunk comment |
x / X |
Remove line/hunk comment |
Ctrl+x |
Clear all comments |
s |
Toggle stepping (no-step mode) |
S |
Toggle strikethrough |
r |
Replay last step (count supported) |
R |
Refresh all files |
Ctrl+f |
Toggle file panel |
Enter |
Focus file list |
] |
Next file (supports count) |
[ |
Previous file (supports count) |
+ / = |
Increase speed |
- |
Decrease speed |
? |
Toggle help |
q / Esc |
Quit (prints comments if any; closes help/path popups) |
Clipboard support uses system tools: pbcopy (macOS), wl-copy / xclip / xsel (Linux), clip (Windows).
Search is case-insensitive regex; invalid patterns fall back to literal matching.
Configuration
Create a config file at ~/.config/oyo/config.toml:
[]
= true # Auto-center on active change (default: true)
= false # EOF overscroll when centering (opt-in)
= true # Show top bar in diff view (default: true)
= "unified" # Default: "unified", "split", "evolution", or "blame"
= false # Wrap long lines (default: false, uses horizontal scroll)
= "off" # "off", "on", or "counts"
= false # Show scrollbar (default: false)
= false # Show strikethrough on deleted text
= true # Show +/- sign column (unified/evolution)
= true # Enable stepping (false = no-step mode)
[]
= "none" # "none" | "step" | "file"
= "none" # "none" | "hunk" | "file"
# [ui.diff]
# bg = false # Full-line diff background (true/false)
# fg = "theme" # "theme" or "syntax"
# highlight = "text" # "text" | "word" | "none"
# max_bytes = 16777216 # Defer diffing above this size (bytes)
# full_context_max_bytes = 2097152 # Full-context render up to this size (bytes)
# defer = true # Defer large diffs and compute in background
# idle_ms = 250 # Idle time before background diff compute
# extent_marker = "neutral" # "neutral" or "diff"
# extent_marker_scope = "progress" # "progress" or "hunk"
# extent_marker_context = false # show extent markers on unchanged lines
# [ui.blame]
# enabled = false # Show git blame hints (opt-in)
# mode = "one_shot" # "one_shot" or "toggle"
# hunk_hint = true # Show blame hint when jumping to a hunk
# [ui.time]
# mode = "relative" # "relative" | "absolute" | "custom"
# format = "[year]-[month]-[day] [hour]:[minute]" # Used when mode = "custom"
# [ui.split]
# align_lines = false # Insert blanks to keep split panes aligned
# align_fill = "╱" # Fill character for aligned blanks (empty = no marker)
# [ui.evo]
# syntax = "context" # "context" (non-diff only) or "full" (diff + context)
# [ui.unified]
# modified_step_mode = "mixed" # "mixed" or "modified" (unified pane only)
# theme = { name = "tokyonight" } # Built-ins listed below
= "▶" # Marker for primary active line (single-width char recommended)
= "◀" # Right pane marker (optional, defaults to ◀)
= "▌" # Left pane extent marker (Left Half Block)
= "▐" # Right pane extent marker (optional, defaults to ▐)
= false # Start in zen mode (minimal UI)
[]
= "on" # "on" or "off"
# theme = "tokyonight" # builtin name or "custom.tmTheme" (from ~/.config/oyo/themes)
# # default: ui.theme.name, fallback to "ansi"
# [ui.syntax.warmup]
# active_lines = 100 # lines per tick while navigating
# pending_lines = 300 # lines per tick while catching up to a pending checkpoint
# idle_lines = 1000 # lines per tick while idle
# debounce_ms = 80 # wait before warming a new viewport target
[]
= 200 # Autoplay interval in milliseconds
= false # Start with autoplay enabled
= false # Enable fade animations
= 150 # Animation duration per phase (ms)
= true # Auto-step to first change when entering a file
= true # Auto-step when file would be blank at step 0 (new files)
[]
= true # Show file panel in multi-file mode
= 30 # File panel width (columns)
= "active" # Per-file +/- counts: active, focused, all, off
[]
= true # Jump to first hunk when entering a file in no-step mode
[]
= "repo" # "changed" | "repo" (git-aware via ls-files)
= "auto" # "auto" | "builtin" | "fzf"
Example config:
[]
= false
= true
= false
= "unified"
= false
= true
[]
= true
[]
= "tokyonight"
[]
= "syntax"
= true
= "text"
= 16777216
= 2097152
= true
= 250
= "diff"
= "hunk"
[]
= "on"
[]
= "full"
[]
= true
[]
= 200
= true
= 150
= false
[]
= "repo"
= "auto"
Config is loaded from (in priority order):
$XDG_CONFIG_HOME/oyo/config.toml~/.config/oyo/config.toml- Platform-specific (e.g.,
~/Library/Application Support/oyo/config.tomlon macOS)
Theme and syntax theme configuration is documented in THEME.md.
Diff styling previews are available in DIFF_PREVIEWS.md.
How It Works
Stepping applies changes in file order. The view renders applied changes, highlights the active change, and keeps pending changes muted.
Development
# Build everything
# Run tests
# Run CLI in development
