Skip to content

feat: add watch mode for auto-refreshing diffs#96

Merged
dlvhdr merged 18 commits intodlvhdr:mainfrom
vertti:feat/watch-mode
Mar 21, 2026
Merged

feat: add watch mode for auto-refreshing diffs#96
dlvhdr merged 18 commits intodlvhdr:mainfrom
vertti:feat/watch-mode

Conversation

@vertti
Copy link
Copy Markdown
Contributor

@vertti vertti commented Mar 5, 2026

Summary

When working on a branch, it's useful to leave diffnav open in a terminal tab and have it automatically reflect the latest changes. This adds --watch mode, which periodically re-runs a diff command and refreshes the TUI.

diffnav --watch
diffnav --watch --watch-cmd "git diff main..." --watch-interval 5s

Details

  • New flags: --watch (-w), --watch-cmd (default: git diff), --watch-interval (default: 2s)
  • Watch tick/result message loop with in-flight guard to prevent overlapping fetches
  • Stderr discarded in watch command execution to avoid corrupting the TUI
  • ClearCache on diffviewer for clean re-renders on refresh
  • Active watch command shown in footer bar
  • README updated with watch mode flags and usage examples

@dlvhdr
Copy link
Copy Markdown
Owner

dlvhdr commented Mar 6, 2026

hmm I a bit wary of introducing this complexity. How do the alternatives look? e.g. using the watch command or entr

@vertti
Copy link
Copy Markdown
Contributor Author

vertti commented Mar 6, 2026

Totally fair concern! I looked into this before going the built-in route.

The core problem is that watch, entr, and watchexec all work by restarting the process. That means stdin gets closed and all TUI state (cursor position, selected file, scroll offset) is lost on every change. There's no way for an external tool to preserve in-memory state across restarts.

For what it's worth, every TUI app I checked builds refresh in for the same reason:

  • lazygit — polls file changes on a time.Ticker (10s default)
  • k9s — built-in refreshRate config (2s default)
  • gitui — built-in filesystem watcher with polling fallback

The implementation here follows the standard Bubble Tea tick -> fetch -> message pattern (~60 lines of actual logic), so it shouldn't add much maintenance surface.

That said, if you had a specific approach in mind that I haven't considered, happy to explore it!

@dlvhdr
Copy link
Copy Markdown
Owner

dlvhdr commented Mar 6, 2026

ok thanks for the detailed response - looks like you did your research :)
I'll go over the code in the next few days and thanks for the contrib!

Copy link
Copy Markdown
Owner

@dlvhdr dlvhdr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good.


cfg.Watch = config.WatchConfig{
Enabled: watchFlag,
Cmd: watchCmd,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about these living in the config. You'd usually want to pipe
stuff into diffnav. I think a shell alias is more appropriate. I am down with

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your comment got cut off — "I am down with" ...?

Two options I see:

  1. Remove WatchConfig from Config struct entirely, pass watch params as a separate arg to ui.New()
  2. Keep as-is — WatchConfig is already excluded from YAML persistence with yaml:"-"

Let me know which you'd prefer (or what you had in mind).

Copy link
Copy Markdown
Owner

@dlvhdr dlvhdr Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh man sorry about that!

Having a default for the watch interval makes sense.
Do you have a use case for having a default watch cmd?

So the config would look like:

defaults:
  watchInterval: 2s

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — no real use case for a default watch command in config. WatchConfig is already yaml:"-" so nothing is persisted. I've made --watch-cmd imply --watch so users don't need both flags. If config-file watchInterval is wanted later, happy to add that as a follow-up.

@dlvhdr
Copy link
Copy Markdown
Owner

dlvhdr commented Mar 15, 2026

Nit: I think --watch-cmd implies --watch, right?

Maybe we can just remove the --watch flag for simplicity and if a user wants he can pass it. wdyt? that way we're also slightly less coupled to git but I don't think it matters :D

@dlvhdr dlvhdr force-pushed the feat/watch-mode branch 4 times, most recently from 8ba46f3 to c6342e4 Compare March 15, 2026 21:54
@vertti
Copy link
Copy Markdown
Contributor Author

vertti commented Mar 21, 2026

@dlvhdr Re: --watch-cmd implying --watch — done, --watch-cmd now enables watch mode automatically. Updated README examples accordingly.

vertti and others added 18 commits March 21, 2026 14:26
Add --watch/-w, --watch-cmd, and --watch-interval flags to enable
periodic diff command execution and UI refresh.
RunCmd executes a shell command and returns stdout with ANSI stripped.
Allows resetting the diff cache when input changes (needed for watch mode).
Schedule periodic ticks that re-run the watch command, compare output,
and refresh the file tree and diff viewer when changes are detected.
Preserves cursor position across refreshes. Empty diffs no longer quit
when watch mode is enabled.
In watch mode, skip stdin and run the watch command for initial data.
Allow empty initial output so the TUI starts and keeps ticking.
Displays "watching: <cmd>" in the footer when watch mode is active.
- Add watchInFlight guard to prevent overlapping fetches on slow commands
- Discard stderr in watch.RunCmd to avoid corrupting TUI display
- Use log.Warn consistently for transient watch command failures
- Add watch mode flags and usage section to README
- Add watch example to cobra Example string
Passing --watch-cmd now automatically enables watch mode, so users
don't need to specify both --watch and --watch-cmd.
@vertti vertti requested a review from dlvhdr March 21, 2026 12:29
Copy link
Copy Markdown
Owner

@dlvhdr dlvhdr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you!

@dlvhdr dlvhdr merged commit e616b2a into dlvhdr:main Mar 21, 2026
2 checks passed
@vertti vertti deleted the feat/watch-mode branch March 23, 2026 07:10
@jquintanilla4
Copy link
Copy Markdown

thanks for adding this feature :)

tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Mar 26, 2026
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [dlvhdr/diffnav](https://github.com/dlvhdr/diffnav) | minor | `v0.10.0` → `v0.11.0` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>dlvhdr/diffnav (dlvhdr/diffnav)</summary>

### [`v0.11.0`](https://github.com/dlvhdr/diffnav/releases/tag/v0.11.0)

[Compare Source](dlvhdr/diffnav@v0.10.0...v0.11.0)

### v0.11.0

#### Notable Features

- Search (accessed via <kbd>t</kbd>) now shows the filename first and has icons
- <kbd>n/p/N</kbd> is used to navigate to next/previous file
- If used with `git show`, and there's a commit preamble present, pressing <kbd>m</kbd> will show the commit info. It's also shown in the header
- There's finally a scrollbar in the diffview
- Thanks to new contributor [@&#8203;vertti](https://github.com/vertti) `diffnav` now supports a `watch` flag which will run `git diff` by default (but can be controlled). It will run the command while maintaining the UI state

Once again, thanks to [@&#8203;pablospe](https://github.com/pablospe) for his awesome contributions!

#### Changelog

- [`0957bb1`](dlvhdr/diffnav@0957bb1) chore: upgrade deps ([#&#8203;114](dlvhdr/diffnav#114))
- [`4a31deb`](dlvhdr/diffnav@4a31deb) ci: add basic CI workflow ([#&#8203;113](dlvhdr/diffnav#113))
- [`dc735a5`](dlvhdr/diffnav@dc735a5) feat(search): show filename first + icons ([#&#8203;105](dlvhdr/diffnav#105))
- [`822adc6`](dlvhdr/diffnav@822adc6) feat: Add shift+tab as alias for tab to switch panels ([#&#8203;109](dlvhdr/diffnav#109))
- [`41f1c16`](dlvhdr/diffnav@41f1c16) feat: add F1 key to toggle help overlay ([#&#8203;101](dlvhdr/diffnav#101))
- [`c899b9e`](dlvhdr/diffnav@c899b9e) feat: add n (next file) and p/N (previous file) shortcuts to jump between files ([#&#8203;98](dlvhdr/diffnav#98))
- [`e651360`](dlvhdr/diffnav@e651360) feat: add scrollbar to diff viewer ([#&#8203;112](dlvhdr/diffnav#112))
- [`e616b2a`](dlvhdr/diffnav@e616b2a) feat: add watch mode for auto-refreshing diffs ([#&#8203;96](dlvhdr/diffnav#96))
- [`19503d8`](dlvhdr/diffnav@19503d8) feat: commit info overlay with `m` key ([#&#8203;103](dlvhdr/diffnav#103))
- [`e215560`](dlvhdr/diffnav@e215560) feat: show commit info when used as a pager for git show ([#&#8203;97](dlvhdr/diffnav#97))
- [`2898ff7`](dlvhdr/diffnav@2898ff7) fix: sidebar search crashes ([#&#8203;95](dlvhdr/diffnav#95))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My44OS42IiwidXBkYXRlZEluVmVyIjoiNDMuODkuNiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiUmVub3ZhdGUgQm90IiwiYXV0b21hdGlvbjpib3QtYXV0aG9yZWQiLCJkZXBlbmRlbmN5LXR5cGU6Om1pbm9yIl19-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants