oyo 0.1.63

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
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
# Config reference

Use this page to find Oyo config keys and defaults.

Create your config file at `~/.config/oyo/config.toml`.

You can also pass extra config files. Oyo loads your user config first, then merges each extra file in order:

```sh
oy --config /tmp/oyo-plugin.toml
```

Extra config files append `review.hooks`, `review.actions` and `selection.actions`. They merge tables, such as `keybindings.normal`.

## Config file locations

Oyo loads the first config file it finds:

1. `$XDG_CONFIG_HOME/oyo/config.toml`.
2. `~/.config/oyo/config.toml`.
3. The platform config directory, such as `~/Library/Application Support/oyo/config.toml` on macOS.

## Change common settings interactively

Press `g s` in Oyo to open Settings. Settings are grouped into General, Diff, Blame, Time, Files, Playback and Appearance.

Changes apply to the current session immediately but do not update your config until you select Save. Use Left and Right to adjust the selected value. Press Enter or Space to open either theme picker.

Revert restores the values from when you opened Settings. Reset to defaults stages Oyo's built-in defaults without saving them. If you leave with unsaved changes, choose Save, Discard or Cancel. Discard also restores the values from when you opened Settings. Oyo preserves unrelated keys, comments and table order when saving.

The Git ignore setting applies after you restart Oyo because directory scan options are set before the interface opens. Other settings in this tab apply during the current session. Quitting Oyo discards unsaved settings changes.

## Minimal config

```toml
[ui]
view_mode = "unified"
stepping = false
watch = true
line_wrap = false

[ui.diff]
fg = "syntax"
bg = true
highlight = "word"

[ui.syntax]
mode = "on"

[files]
panel_visible = true
panel_width = 30
panel_position = "left"
```

## UI

Use `[ui]` for general display and navigation defaults.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `zen` | `true`, `false` | `false` | Starts with minimal UI chrome |
| `topbar` | `true`, `false` | `true` | Shows the top bar in the diff view |
| `auto_center` | `true`, `false` | `true` | Keeps the active change centred while stepping |
| `watch` | `true`, `false` | `true` | Refreshes changed files on disk |
| `overscroll` | `true`, `false` | `false` | Allows extra end-of-file scroll while centring |
| `confirm_quit` | `true`, `false` | `true` | Asks for confirmation before quitting the TUI |
| `view_mode` | `unified`, `split`, `evolution`, `blame`, `preview` | `unified` | Sets the default view mode |
| `line_wrap` | `true`, `false` | `false` | Wraps long lines instead of horizontal scrolling |
| `fold_context` | `off`, `expandable` | `expandable` | Sets whether Oyo folds long unchanged blocks |
| `fold_context_lines` | non-negative integer | `3` | Keeps this many context lines on each side of a fold |
| `scrollbar` | `true`, `false` | `true` | Shows the diff and file panel scrollbars |
| `strikethrough_deletions` | `true`, `false` | `false` | Strikes through deleted text |
| `gutter_signs` | `true`, `false` | `true` | Shows added and removed signs in unified and evolution views |
| `stepping` | `true`, `false` | `false` | Starts in step mode |
| `primary_marker` | string | built in | Sets the active-line marker for the left pane or unified view |
| `primary_marker_right` | string | built in | Sets the active-line marker for the right pane |
| `extent_marker_left` | string | built in | Sets the hunk extent marker for the left pane or unified view |
| `extent_marker_right` | string | built in | Sets the hunk extent marker for the right pane |
| `extent_marker_deleted` | string | built in | Sets the hunk extent marker for deleted lines |
| `extent_marker` | string | built in | Legacy name for `extent_marker_left` |

Long unchanged blocks are expandable by default. Oyo creates a fold when at least 8 lines remain hidden after preserving edge context. Set `fold_context = "off"` to show full context by default. Set `fold_context_lines = 0` for the most compact expandable view. The older `on` value remains an alias for `expandable`.

Press `f` to toggle full and folded context. Search checks rendered lines. Press `F` to expand all folds before searching hidden context.

```toml
[ui]
zen = false
topbar = true
auto_center = true
watch = true
overscroll = false
confirm_quit = true
view_mode = "unified"
line_wrap = false
fold_context = "expandable"
fold_context_lines = 3
scrollbar = true
strikethrough_deletions = false
gutter_signs = true
stepping = false
extent_marker_left = "┃"
extent_marker_right = "▐"
extent_marker_deleted = "╏"

# Use "off" to show full context by default.
# Use fold_context_lines = 0 for maximum compaction.

[ui.toasts]
enabled = true
position = "bottom_right"
```

### Toasts

Use `[ui.toasts]` to turn toast notifications on or off and choose where they appear.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `enabled` | `true`, `false` | `true` | Shows short notifications for actions such as copy and toggles |
| `position` | `top_left`, `top_right`, `bottom_left`, `bottom_right`, `center` | `bottom_right` | Sets where toast notifications appear |

## Diff display

Use `[ui.diff]` to control diff colours, inline highlights and large-file behaviour.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `bg` | `true`, `false` | `true` | Shows full-line diff backgrounds |
| `fg` | `theme`, `syntax` | `syntax` | Chooses diff text colours |
| `highlight` | `text`, `word`, `none` | `word` | Controls inline changed-span highlights |
| `max_bytes` | integer | `16777216` | Defers diffing above this file size |
| `full_context_max_bytes` | integer | `2097152` | Uses full-context rendering up to this file size |
| `defer` | `true`, `false` | `true` | Computes large diffs in the background |
| `idle_ms` | integer | `250` | Waits this many milliseconds before background diffing starts |
| `extent_marker` | `neutral`, `diff` | `diff` | Chooses hunk extent marker colour |
| `extent_marker_scope` | `progress`, `hunk` | `hunk` | Chooses which lines get diff colours while stepping |
| `extent_marker_context` | `true`, `false` | `false` | Shows extent markers on unchanged context lines |
| `preview_change_bars` | `true`, `false` | `true` | Shows change bars in source, Markdown, CSV and structured previews |

```toml
[ui.diff]
bg = true
fg = "syntax"
highlight = "word"
max_bytes = 16777216
full_context_max_bytes = 2097152
defer = true
idle_ms = 250
extent_marker = "diff"
extent_marker_scope = "hunk"
extent_marker_context = false
preview_change_bars = true
```

See [diff behaviour](./DIFF_VIEWER.md) and [diff styling previews](./DIFF_PREVIEWS.md).

## Syntax highlighting

Use `[ui.syntax]` to turn syntax highlighting on or off and choose a syntax theme.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `mode` | `on`, `off` | `on` | Enables syntax highlighting |
| `theme` | theme name or path | empty | Uses `ui.theme.name`, then falls back to `ansi` |

Oyo also loads custom Sublime Text grammars from the `syntaxes` directory beside `config.toml`. On Linux, this is normally `~/.config/oyo/syntaxes`. On macOS, Oyo also checks `~/Library/Application Support/oyo/syntaxes`. Add `.sublime-syntax` files there and restart Oyo. Custom grammars take precedence over built-in grammars with the same file extension.

Use `[ui.syntax.warmup]` to control background syntax preparation.

| Key | Default | What it does |
| --- | --- | --- |
| `active_lines` | `100` | Lines per tick while navigating |
| `pending_lines` | `300` | Lines per tick while catching up |
| `idle_lines` | `1000` | Lines per tick while idle |
| `debounce_ms` | `80` | Wait time before warming a new viewport target |

```toml
[ui.syntax]
mode = "on"
theme = "tokyonight"

[ui.syntax.warmup]
active_lines = 100
pending_lines = 300
idle_lines = 1000
debounce_ms = 80
```

See [theme configuration](./THEME.md).

## View-specific settings

Use these tables to change one view mode.

```toml
[ui.unified]
modified_step_mode = "mixed" # "mixed" or "modified"

[ui.split]
align_lines = true
align_fill = "/"

[ui.evo]
syntax = "context" # "context" or "full"
```

| Table | Key | Values | Default | What it does |
| --- | --- | --- | --- | --- |
| `[ui.unified]` | `modified_step_mode` | `mixed`, `modified` | `mixed` | Chooses how modified lines render while stepping |
| `[ui.split]` | `align_lines` | `true`, `false` | `true` | Inserts blank rows to keep panes aligned |
| `[ui.split]` | `align_fill` | string | built in | Fills aligned blank rows |
| `[ui.evo]` | `syntax` | `context`, `full` | `context` | Chooses syntax scope in evolution view |

## Blame and time

Use `[ui.blame]` for git blame hints.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `enabled` | `true`, `false` | `false` | Shows blame hints |
| `mode` | `one_shot`, `toggle` | `one_shot` | Chooses how blame display behaves |
| `hunk_hint` | `true`, `false` | `true` | Shows blame hint when jumping to a hunk |

Use `[ui.time]` for time display.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `mode` | `relative`, `absolute`, `custom` | `relative` | Chooses time display mode |
| `format` | string | empty | Sets the custom time format |

```toml
[ui.blame]
enabled = false
mode = "one_shot"
hunk_hint = true

[ui.time]
mode = "relative"
format = "[year]-[month]-[day] [hour]:[minute]"
```

## Theme

Use `[ui.theme]` to choose a UI theme.

```toml
[ui.theme]
name = "tokyonight"
mode = "dark"
```

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `name` | theme name | unset | Sets the UI theme |
| `mode` | `dark`, `light` | dark | Chooses the theme variant |
| `defs` | table | empty | Defines reusable colour names |
| `theme` | table | built in | Overrides theme tokens |

See [theme configuration](./THEME.md) for built-in themes, custom themes and theme tokens.

## Navigation wrapping

Use `[navigation.wrap]` to choose what happens at the ends of files or hunks.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `step` | `none`, `step`, `file` | `none` | Chooses step navigation wrapping |
| `hunk` | `none`, `hunk`, `file` | `none` | Chooses hunk navigation wrapping |

```toml
[navigation.wrap]
step = "none"
hunk = "none"
```

## Playback

Use `[playback]` for autoplay, animations and automatic first-step behaviour.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `speed` | integer | `200` | Autoplay delay in milliseconds |
| `autoplay` | `true`, `false` | `false` | Starts with autoplay enabled |
| `animation` | `true`, `false` | `true` | Enables step animations |
| `animation_duration` | integer | `120` | Animation duration in milliseconds |
| `auto_step_on_enter` | `true`, `false` | `true` | Steps to the first change when entering a file at step 0 |
| `auto_step_blank_files` | `true`, `false` | `true` | Steps when a new file would otherwise be blank at step 0 |

```toml
[playback]
speed = 200
autoplay = false
animation = true
animation_duration = 120
auto_step_on_enter = true
auto_step_blank_files = true
```

## Files

Use `[files]` to configure the file panel.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `panel_visible` | `true`, `false` | `true` | Shows the file panel in multi-file mode |
| `panel_width` | integer | `30` | Sets file panel width in columns |
| `panel_position` | `left`, `right` | `left` | Sets which side the file panel uses |
| `counts` | `active`, `focused`, `all`, `off` | `active` | Chooses when per-file counts appear |

Use `[files.scan]` for manual directory scans.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `git_ignore` | `auto`, `true`, `false`, `on`, `off` | `auto` | Uses git ignore rules during scans |
| `ignore_globs` | array of strings | VCS directories | Excludes paths during scans |

```toml
[files]
panel_visible = true
panel_width = 30
panel_position = "left"
counts = "active"

[files.scan]
git_ignore = "auto"
ignore_globs = [".git/**", ".jj/**", ".hg/**", ".svn/**"]
```

## No-step mode

Use `[no_step]` for scroll mode behaviour.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `auto_jump_on_enter` | `true`, `false` | `true` | Jumps to the first hunk when entering a file in no-step mode |

```toml
[no_step]
auto_jump_on_enter = true
```

## Editor

Use `[editor]` for the open-in-editor action.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `command` | string | `$VISUAL`, then `$EDITOR`, then `vi` | Sets the editor command |
| `args` | array of strings | unset | Sets argument templates. Supports `{file}` and `{line}` |
| `open_at_line` | `true`, `false` | `true` | Passes `+line` before the file path when `args` is unset |
| `wait` | `true`, `false` | `true` | Suspends Oyo until the editor exits. Set to `false` for a detached GUI editor |

```toml
[editor]
command = "nvim"
args = ["+{line}", "{file}"]
open_at_line = true
wait = true
```

A detached editor keeps Oyo active:

```toml
[editor]
command = "zed"
args = ["{file}:{line}"]
open_at_line = true
wait = false
```

With `wait = false`, use watch mode or press `R` after saving to refresh the diff.

## Comments and mentions

Use `[comments.mentions]` to configure file mentions in comments.

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `file_scope` | `changed`, `repo` | `repo` | Chooses files available for mentions |
| `finder` | `auto`, `builtin`, `fzf` | `auto` | Chooses how Oyo ranks mention candidates |

```toml
[comments.mentions]
file_scope = "repo"
finder = "auto"
```

## Review

Use `[review]` to configure saved reviews and where Oyo saves the review database.

```toml
[review]
dir = ".oyo/reviews"
```

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `dir` | path | platform app data directory | Sets the base directory for review databases |
| `incremental` | `true`, `false` | `false` | Starts saved reviews with reviewed files hidden. You can turn it on or off for a session from the command palette |

Relative paths resolve from the current workspace root. This supports Git worktrees and jj workspaces.

GitHub sync uses the built-in `gh` adapter. Use provider config when you need to define another provider command interface.

Pass a remote name to `oy review pull` or `oy review push` when you do not want the default remote.

### Incremental review

Incremental review is off by default. To enable it for saved reviews:

```toml
[review]
incremental = true
```

If you previously enabled this under `[experimental]`, replace `[experimental].incremental_review` with `[review].incremental`. The old setting is no longer read.

See [incremental review](./REVIEW.md#hide-reviewed-files) for how it works.

### Provider command interface

Add custom providers under `[review.providers.<id>]`.

```toml
[review.providers.example]
hosts = ["git.example.com"]

[review.providers.example.commands.whoami]
command = "example-oyo-provider"
args = ["whoami"]

[review.providers.example.commands.pr_get]
command = "example-oyo-provider"
args = ["pr", "get", "{repo}", "{target}"]

[review.providers.example.commands.comments_list]
command = "example-oyo-provider"
args = ["comments", "list", "{repo}", "{number}"]

[review.providers.example.commands.comments_create]
command = "example-oyo-provider"
args = ["comments", "create", "{repo}", "{number}"]

[review.providers.example.commands.comments_update]
command = "example-oyo-provider"
args = ["comments", "update", "{repo}", "{number}"]

[review.providers.example.commands.comments_delete]
command = "example-oyo-provider"
args = ["comments", "delete", "{repo}", "{number}"]
```

Provider command keys are:

- `whoami`
- `pr_find`
- `pr_get`
- `comments_list`
- `comments_create`
- `comments_update`
- `comments_delete`

Each command supports:

| Key | Values | Default | What it does |
| --- | --- | --- | --- |
| `command` | string | none | Sets the executable |
| `args` | list of strings | `[]` | Sets command arguments |
| `timeout_ms` | number | `30000` | Sets the command timeout |

Provider commands return Oyo-shaped JSON. Mutation commands read Oyo-shaped JSON from standard input.

See [provider command contract](./REVIEW.md#provider-command-contract).

## Review hooks and actions

Use `[[review.hooks]]` to run commands after review events.

```toml
[[review.hooks]]
id = "review-ready"
on = "review_ready"
command = ".oyo/hooks/review-ready"
args = []
stdin = "json"
blocking = true
timeout_ms = 30000
```

Use `[[review.actions]]` to expose commands in the UI.

```toml
[[review.actions]]
id = "send"
label = "Send review"
key = "ctrl-s"
on = "review_ready"
command = ".oyo/hooks/send-review"
args = []
stdin = "json"
blocking = true
timeout_ms = 30000
save_editor = true
show = ["command_palette", "review_editor"]
```

Review events are:

- `comment_saved`
- `comment_deleted`
- `comments_cleared`
- `review_ready`

`stdin` can be `json` or `none`.

See [review hooks](./REVIEW_HOOKS.md).

## Selection actions

Use `[[selection.actions]]` to run commands from the selection toolbar.

```toml
[[selection.actions]]
id = "ask-agent"
label = "Ask agent"
key = "a"
message = "Sent to agent"
failure_message = "Could not send to agent"
command = "my-agent"
args = ["review-selection"]
stdin = "json"
blocking = true
timeout_ms = 30000
```

Oyo sends JSON on standard input by default. The payload includes the selected text, file, repo root, line ranges, view mode, side, scroll offset and selected screen rows.

`stdin` can be `json` or `none`.

If you omit `message` or `failure_message`, Oyo uses `<label> started` and `<label> failed`.

## Keybindings

Use `[keybindings.<mode>]` tables to override keys.

```toml
[keybindings.global]
open_command_palette = ["ctrl-p"]
open_file_search = ["ctrl-shift-p"]
open_review_grep = ["ctrl-shift-f"]
open_theme_picker = ["ctrl-t"]

[keybindings.normal]
step_down = ["j", "down"]
step_up = ["k", "up"]
open_dashboard = ["ctrl-r"]
start_selection = ["v"]
start_line_selection = ["V"]
start_block_selection = ["ctrl-v"]
toggle_help = ["?"]

[keybindings.review_grep]
toggle_scope = ["tab"]
select_changes = ["alt-d"]
select_everything = ["alt-e"]

[keybindings.review_editor]
save = ["ctrl-s"]
clear = ["ctrl-u"]

[keybindings.selection]
copy = ["y"]
cancel = ["esc"]
show_actions = ["enter"]

[keybindings.search]
cancel = ["esc"]
accept = ["enter"]
clear = ["ctrl-u"]
```

If you omit an action, Oyo keeps the default keys. Set an action to an empty array to unbind it.

See [keybinding actions](./KEYBINDINGS.md).