oyo 0.1.26

CLI TUI for oyo - Step-through diff viewer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
<div align="center">

# oyo

A diff viewer that works **two ways**:
**step through changes** or **review a classic scrollable diff**.


<!-- Demo source: https://github.com/user-attachments/assets/0f43b54b-69fe-4cf3-9221-a7749872342b -->
https://github.com/user-attachments/assets/0f43b54b-69fe-4cf3-9221-a7749872342b

</div>

**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 = false` in config

### 2. Step-through review (default)
Apply changes incrementally and watch the file transform from old → 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)
- **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 `.tmTheme` syntax themes (configurable, with light/dark variants)
- **Configurable**: XDG config file support for customization

## Installation

### Homebrew (macOS/Linux)

```bash
brew install ahkohd/oyo/oy
```

### AUR (Arch Linux)

```bash
paru -S oyo
```

### Cargo

```bash
cargo install oyo
```

## Usage

### Classic diff (scroll-only)

```bash
oy --no-step
# or toggle in-app with `s`
```

### Step-through diff

```bash
oy
```

### Compare files

```bash
oy old.rs new.rs
```

### Compare a file against HEAD

```bash
oy path/to/file.rs
```

Runs a working-tree vs `HEAD` diff for that file (like `git diff path/to/file.rs`).

### Commit picker

```bash
oy view
```

### View modes

```bash
oy old.rs new.rs --view split
oy old.rs new.rs --view evolution
```

### Autoplay

```bash
oy old.rs new.rs --autoplay
oy old.rs new.rs --speed 100
```

### Git ranges

```bash
oy --range HEAD~1..HEAD
oy --range main...feature
```

### Staged changes

```bash
oy --staged
```

---

## Git Integration

### Recommended (`git difftool`)

```bash
git difftool -y --tool=oy
```

`~/.gitconfig`:

```gitconfig
[difftool "oy"]
    cmd = oy "$LOCAL" "$REMOTE"

[difftool]
    prompt = false

[alias]
    d = difftool -y --tool=oy
```

> Note: keep your pager (`less`, `moar`, `moor`) for `git diff`.
> Do **not** set `core.pager` or `interactive.diffFilter` to `oy`.

---

## Jujutsu (jj)

```toml
[ui]
paginate = "never"
diff-formatter = ["oy", "$left", "$right"]

[diff-tools.oy]
command = ["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 |
| `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 (or close help) |

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`:

```toml
[ui]
auto_center = true          # Auto-center on active change (default: true)
overscroll = false         # EOF overscroll when centering (opt-in)
topbar = true               # Show top bar in diff view (default: true)
view_mode = "unified"       # Default: "unified", "split", "evolution", or "blame"
line_wrap = false           # Wrap long lines (default: false, uses horizontal scroll)
fold_context = "off"        # "off", "on", or "counts"
scrollbar = false           # Show scrollbar (default: false)
strikethrough_deletions = false # Show strikethrough on deleted text
gutter_signs = true         # Show +/- sign column (unified/evolution)
stepping = true             # Enable stepping (false = no-step mode)

[navigation.wrap]
step = "none"               # "none" | "step" | "file"
hunk = "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)
# Syntax highlighting:
# - legacy: syntax = "on"    # "on" or "off"
# - table:
#   [ui.syntax]
#   mode = "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
syntax = "on"
# [ui.unified]
# modified_step_mode = "mixed" # "mixed" or "modified" (unified pane only)
# theme = { name = "tokyonight" } # Built-ins listed below
primary_marker = "▶"        # Marker for primary active line (single-width char recommended)
primary_marker_right = "◀"  # Right pane marker (optional, defaults to ◀)
extent_marker = "▌"         # Left pane extent marker (Left Half Block)
extent_marker_right = "▐"   # Right pane extent marker (optional, defaults to ▐)
zen = false                 # Start in zen mode (minimal UI)

[playback]
speed = 200                 # Autoplay interval in milliseconds
autoplay = false            # Start with autoplay enabled
animation = false           # Enable fade animations
animation_duration = 150    # Animation duration per phase (ms)
auto_step_on_enter = true   # Auto-step to first change when entering a file
auto_step_blank_files = true # Auto-step when file would be blank at step 0 (new files)

[files]
panel_visible = true        # Show file panel in multi-file mode
panel_width = 30            # File panel width (columns)
counts = "active"           # Per-file +/- counts: active, focused, all, off

[no_step]
auto_jump_on_enter = true   # Jump to first hunk when entering a file in no-step mode
```

Example config:

```toml
[ui]
zen = false
auto_center = true
overscroll = false
view_mode = "unified"
gutter_signs = false
topbar = true

[ui.blame]
enabled = true

[ui.theme]
name = "tokyonight"

[ui.diff]
fg = "syntax"
bg = true
highlight = "text"
max_bytes = 16777216
full_context_max_bytes = 2097152
defer = true
idle_ms = 250
extent_marker = "diff"
extent_marker_scope = "hunk"

[ui.syntax]
mode = "on"

[ui.evo]
syntax = "full"

[ui.split]
align_lines = true

[playback]
speed = 200
animation = true
animation_duration = 150
autoplay = false
```

Config is loaded from (in priority order):
1. `$XDG_CONFIG_HOME/oyo/config.toml`
2. `~/.config/oyo/config.toml`
3. Platform-specific (e.g., `~/Library/Application Support/oyo/config.toml` on macOS)

Theme and syntax theme configuration is documented in [THEME.md](./docs/THEME.md).

[![diff preview](./assets/ui_syntax_off.png)](./docs/DIFF_PREVIEWS.md)

*Diff styling previews are available in [DIFF_PREVIEWS.md](./docs/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

```bash
# Build everything
cargo build

# Run tests
cargo test

# Run CLI in development
cargo run --bin oy -- old.rs new.rs
```