Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .claude-plugin/skills/revdiff/references/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
brew install umputun/apps/revdiff
```

**Go:**

```bash
go install github.com/umputun/revdiff/app/revdiff@latest
```

Installs the `revdiff` binary into `GOBIN` (defaults to `$(go env GOPATH)/bin`). Add that directory to your `PATH`.

**Binary releases:** download from [GitHub Releases](https://github.com/umputun/revdiff/releases) (deb, rpm, archives for linux/darwin amd64/arm64).

## Claude Code Plugin
Expand Down
8 changes: 4 additions & 4 deletions .claude/rules/gotchas.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2

builds:
- id: revdiff
main: ./app
main: ./app/revdiff
binary: revdiff
env:
- CGO_ENABLED=0
Expand Down
4 changes: 2 additions & 2 deletions .zed/tasks.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[
{
"label": "revdiff: run",
"command": "go run ./app --dbg",
"command": "go run ./app/revdiff --dbg",
"cwd": "$ZED_WORKTREE_ROOT",
"use_new_terminal": true,
"allow_concurrent_runs": false
},
{
"label": "revdiff: run staged",
"command": "go run ./app --staged --dbg",
"command": "go run ./app/revdiff --staged --dbg",
"cwd": "$ZED_WORKTREE_ROOT",
"use_new_terminal": true,
"allow_concurrent_runs": false
Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ REV=$(if $(filter --,$(GIT_REV)),latest,$(GIT_REV))
all: test build

build:
go build -ldflags "-X main.revision=$(REV) -s -w" -o .bin/revdiff.$(BRANCH) ./app
go build -ldflags "-X main.revision=$(REV) -s -w" -o .bin/revdiff.$(BRANCH) ./app/revdiff
cp .bin/revdiff.$(BRANCH) .bin/revdiff

test:
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ sudo dpkg -i revdiff_*.deb
sudo rpm -i revdiff_*.rpm
```

**Go:**

```bash
go install github.com/umputun/revdiff/app/revdiff@latest
```

Installs the `revdiff` binary into `GOBIN` (defaults to `$(go env GOPATH)/bin`). Add that directory to your `PATH`.

**Binary releases:** download from [GitHub Releases](https://github.com/umputun/revdiff/releases) (deb, rpm, archives for linux/darwin amd64/arm64).

## Claude Code Plugin
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
14 changes: 13 additions & 1 deletion app/main.go → app/revdiff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"os"
"path/filepath"
"runtime/debug"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
Expand Down Expand Up @@ -46,7 +47,8 @@ func main() {

// early-exit commands that don't need theme resolution
if opts.Version {
fmt.Printf("version: %s\n", revision)
info, _ := debug.ReadBuildInfo()
fmt.Printf("version: %s\n", buildVersion(revision, info))
os.Exit(0)
}

Expand Down Expand Up @@ -95,6 +97,16 @@ func main() {
}
}

func buildVersion(rev string, info *debug.BuildInfo) string {
if rev != "" && rev != "unknown" {
return rev
}
if info != nil && info.Main.Version != "" && info.Main.Version != "(devel)" {
return info.Main.Version
}
return "unknown"
}

func run(opts options) (int, error) {
// force lipgloss to truecolor when colors are enabled. revdiff's raw-ANSI
// helpers (style.ansiColor) always emit truecolor, but lipgloss respects
Expand Down
24 changes: 24 additions & 0 deletions app/main_test.go → app/revdiff/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,37 @@ import (
"os/exec"
"path/filepath"
"runtime"
"runtime/debug"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestBuildVersion(t *testing.T) {
tests := []struct {
name string
revision string
info *debug.BuildInfo
want string
}{
{name: "ldflags revision wins", revision: "v1.14.0-custom", info: &debug.BuildInfo{Main: debug.Module{Version: "v1.14.0"}}, want: "v1.14.0-custom"},
{name: "ldflags without build info", revision: "v1.14.0-custom", want: "v1.14.0-custom"},
{name: "installed module version", revision: "unknown", info: &debug.BuildInfo{Main: debug.Module{Version: "v1.14.0"}}, want: "v1.14.0"},
{name: "empty revision", info: &debug.BuildInfo{Main: debug.Module{Version: "v1.14.0"}}, want: "v1.14.0"},
{name: "development build", revision: "unknown", info: &debug.BuildInfo{Main: debug.Module{Version: "(devel)"}}, want: "unknown"},
{name: "empty module version", revision: "unknown", info: &debug.BuildInfo{}, want: "unknown"},
{name: "missing build info", revision: "unknown", want: "unknown"},
{name: "missing revision and build info", want: "unknown"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, buildVersion(tt.revision, tt.info))
})
}
}

type errWriter struct{}

func (errWriter) Write([]byte) (int, error) { return 0, errors.New("write failed") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,7 @@ func TestCodexPlanReviewHook(t *testing.T) {
liveTranscript := filepath.Join(
root,
"app",
"revdiff",
"testdata",
"plugin-exit-code",
"rollout-2026-07-16T10-54-26-session-current.jsonl",
Expand Down Expand Up @@ -1709,7 +1710,7 @@ func testRepoRoot(t *testing.T) string {
t.Helper()
wd, err := os.Getwd()
require.NoError(t, err)
return filepath.Dir(wd)
return filepath.Dir(filepath.Dir(wd))
}

func runTestCmd(t *testing.T, r cmdReq) cmdResult {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion app/themes_test.go → app/revdiff/themes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,7 @@ func parsePatchedConfig(t *testing.T, path string) (options, error) {
func TestPatchConfigTheme_testdataRoundTrip(t *testing.T) {
tests := []struct {
name string
fixture string // path under app/testdata/themes/
fixture string
}{
{name: "good config (theme already in [Application Options])", fixture: "good.ini"},
{name: "no theme line, trailing [color options]", fixture: "no_theme.ini"},
Expand Down
8 changes: 4 additions & 4 deletions app/ui/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@
//
// Theme discovery and persistence are accessed through the [ThemeCatalog] interface defined
// in model.go. This package does not import app/theme or app/fsutil — the concrete adapter
// is wired in app/themes.go, composing theme.Catalog with config file persistence.
// is wired in app/revdiff/themes.go, composing theme.Catalog with config file persistence.
//
// Intra-line word-diff algorithms and the shared highlight marker insertion engine live
// in the [worddiff] sub-package (app/ui/worddiff/). It owns the tokenizer, LCS algorithm,
// line pairing, similarity gate, and ANSI-aware highlight marker insertion used by both
// word-diff and search highlighting. Model holds the worddiff type through a consumer-side
// interface (wordDiffer) defined in model.go; concrete *worddiff.Differ is injected via
// ModelConfig.WordDiffer wired in app/main.go.
// ModelConfig.WordDiffer wired in app/revdiff/main.go.
//
// Color and style management lives in the [style] sub-package (app/ui/style/).
// It owns all hex-to-ANSI conversion, lipgloss style construction, SGR state tracking,
Expand All @@ -64,7 +64,7 @@
// including cursor/offset management, entry parsing, and rendering logic.
// Model holds sidepane types through consumer-side interfaces (FileTreeComponent,
// TOCComponent) defined in model.go; concrete construction is injected via
// ModelConfig.NewFileTree and ModelConfig.ParseTOC factory closures wired in app/main.go.
// ModelConfig.NewFileTree and ModelConfig.ParseTOC factory closures wired in app/revdiff/main.go.
//
// Layered popup UI lives in the [overlay] sub-package (app/ui/overlay/).
// It owns help, annotation list, theme selector, and file picker overlays — all popup state
Expand All @@ -74,7 +74,7 @@
// coordinator enforces mutual exclusivity (one overlay at a time) and routes
// key events and compose calls to the active overlay. Model holds the Manager
// through a consumer-side interface (overlayManager) defined in model.go;
// concrete *overlay.Manager is injected via ModelConfig.Overlay wired in app/main.go.
// concrete *overlay.Manager is injected via ModelConfig.Overlay wired in app/revdiff/main.go.
//
// The key interfaces consumed by Model are [Renderer] (provides changed files and diffs),
// [SyntaxHighlighter] (provides ANSI-highlighted lines), [Blamer] (provides blame data),
Expand Down
20 changes: 10 additions & 10 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ TUI for reviewing diffs, files, and documents with inline annotations, built wit

```
┌─────────────────────────────────────────────────────┐
│ app/ — composition root (package main)
│ app/revdiff/ — composition root (package main) │
│ main.go — main(), early-exit flow │
│ config.go — options, parseArgs, config IO │
│ stdin.go — stdin validation, /dev/tty │
Expand Down Expand Up @@ -35,7 +35,7 @@ TUI for reviewing diffs, files, and documents with inline annotations, built wit

## Package Responsibilities

### app/ (composition root)
### app/revdiff/ (composition root)

`package main` is the composition root, split across files by concern:

Expand Down Expand Up @@ -169,7 +169,7 @@ across files by concern to keep files under ~500 lines:
`flushWheelPending()` is called from `handleWheelDebounce`, `handleKey`, `handleResize`, and
`handleBlameLoaded` (any path that runs `syncViewportToCursor` or reads `m.nav.diffCursor` must
flush first). Mouse tracking is enabled program-wide via `tea.WithMouseCellMotion()` in
`app/main.go` unless `--no-mouse` / `REVDIFF_NO_MOUSE` is set
`app/revdiff/main.go` unless `--no-mouse` / `REVDIFF_NO_MOUSE` is set

Each source file has a matching `_test.go`.

Expand All @@ -196,7 +196,7 @@ mini-models.

**Theme boundary** — `app/ui` does not import `app/theme` or `app/fsutil`. Theme discovery and
persistence are accessed through the `ThemeCatalog` interface (defined in `model.go`), with a
concrete adapter wired in `app/themes.go`.
concrete adapter wired in `app/revdiff/themes.go`.

### app/ui/style/ — color, style resolution, and display helpers

Expand Down Expand Up @@ -342,7 +342,7 @@ File layout:
Bundled themes: revdiff, catppuccin-mocha, catppuccin-latte, dracula, gruvbox, nord, solarized-dark.
Community themes live in `themes/gallery/`.

23 color keys mapped via `colorFieldPtrs()` in `app/themes.go` — single source of truth for color
23 color keys mapped via `colorFieldPtrs()` in `app/revdiff/themes.go` — single source of truth for color
key to struct field mapping.

### app/annotation/ — annotation store
Expand Down Expand Up @@ -398,7 +398,7 @@ On a signal-delivered exit (a SIGHUP from a dropped SSH/tmux client, or a SIGTER
`main.go` invokes this save as a crash-recovery net and stops there — history only, never the `-o`
output. This is a deliberate semantic change: a signal-delivered SIGTERM no longer writes `-o`,
because a signal is not the deliberate handoff that `q`/`O` perform. The wiring lives at the
composition root — `shutdownGuard` in `app/signal.go` plus `tea.WithoutSignalHandler()` so revdiff
composition root — `shutdownGuard` in `app/revdiff/signal.go` plus `tea.WithoutSignalHandler()` so revdiff
owns SIGHUP/SIGTERM instead of bubbletea. SIGINT is caught and drained so a Ctrl-C during an
external `$EDITOR` does not quit revdiff. The guard is stopped (default signal disposition restored
for all three) before `finalize()` runs, so a slow or hung finalize (`saveHistory` shells out to
Expand Down Expand Up @@ -434,7 +434,7 @@ belong to the consumer.
`OpenFilePicker()`, `OpenInfo()`, `UpdateInfo()`, `Close()`, `HandleKey()`, `HandleMouse()`,
`Compose()`; implemented by `overlay.Manager`
- **`ThemeCatalog`** — `Entries()`, `Resolve()`, `Persist()`; implemented by `themeCatalog` adapter
in `app/themes.go` (composes `theme.Catalog` + config persistence)
in `app/revdiff/themes.go` (composes `theme.Catalog` + config persistence)
- **`ExternalEditor`** — `Command(content)` for annotation temp-file editing,
`SourceCommand(path string, line int)` for opening source files; implemented by `editor.Editor`
(default wiring via `ModelConfig.Editor`; stubbed in tests)
Expand Down Expand Up @@ -601,14 +601,14 @@ User presses '?' / '@' / 'T' / 'P' / 'i'
- **History**: `~/.config/revdiff/history/` (auto-save dir)

Theme precedence: `--theme` overwrites all 23 color fields + chroma-style, ignoring `--color-*`
flags or env vars. Applied via `applyTheme()` in `app/themes.go` which directly overwrites
flags or env vars. Applied via `applyTheme()` in `app/revdiff/themes.go` which directly overwrites
`opts.Colors.*` fields after `parseArgs()`.

Adding a new color requires changes in three places: `theme.go` colorKeys + options struct +
`colorFieldPtrs()` in `app/themes.go`.
`colorFieldPtrs()` in `app/revdiff/themes.go`.

Theme ownership is split by concern: `app/theme` owns discovery/loading/installation via `Catalog`,
`app/ui` consumes a `ThemeCatalog` interface for selector/preview/apply, and `app/themes.go` wires a
`app/ui` consumes a `ThemeCatalog` interface for selector/preview/apply, and `app/revdiff/themes.go` wires a
thin adapter composing `theme.Catalog` + config file persistence.

## Input Modes
Expand Down
8 changes: 1 addition & 7 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
# so build straight from the vendored tree with no network fetch.
vendorHash = null;

subPackages = [ "app" ];
subPackages = [ "app/revdiff" ];

# Tests need the git working tree, which is absent in the Nix sandbox.
doCheck = false;
Expand All @@ -44,12 +44,6 @@
"-X main.revision=${version}"
];

# The main package lives in ./app, so the produced binary is named
# `app`; rename it to `revdiff`.
postInstall = ''
mv $out/bin/app $out/bin/revdiff
'';

meta = {
description = "TUI for reviewing diffs, files, and documents with inline annotations";
homepage = "https://github.com/umputun/revdiff";
Expand Down
8 changes: 8 additions & 0 deletions plugins/codex/skills/revdiff/references/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
brew install umputun/apps/revdiff
```

**Go:**

```bash
go install github.com/umputun/revdiff/app/revdiff@latest
```

Installs the `revdiff` binary into `GOBIN` (defaults to `$(go env GOPATH)/bin`). Add that directory to your `PATH`.

**Binary releases:** download from [GitHub Releases](https://github.com/umputun/revdiff/releases) (deb, rpm, archives for linux/darwin amd64/arm64).

## Codex Plugin
Expand Down
3 changes: 3 additions & 0 deletions site/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ <h3>Debian/Ubuntu</h3>
<h3>RPM-based (Fedora, RHEL)</h3>
<div class="code-block"><code>sudo rpm -i revdiff_*.rpm</code></div>
<p>Download the latest <code>.rpm</code> for your architecture from <a href="https://github.com/umputun/revdiff/releases">GitHub Releases</a>.</p>
<h3>Go</h3>
<div class="code-block"><code>go install github.com/umputun/revdiff/app/revdiff@latest</code></div>
<p>Installs the <code>revdiff</code> binary into <code>GOBIN</code> (defaults to <code>$(go env GOPATH)/bin</code>). Add that directory to your <code>PATH</code>.</p>
<h3>Binary releases</h3>
<p>Download from <a href="https://github.com/umputun/revdiff/releases">GitHub Releases</a> &mdash; deb, rpm, archives for linux/darwin amd64/arm64.</p>

Expand Down