Skip to content

feat: Beyond-Compare-style side-by-side viewer (PySide6) - #13

Merged
longvo92 merged 6 commits into
mainfrom
claude/file-comparison-app-plan-7fdea7
Jul 23, 2026
Merged

feat: Beyond-Compare-style side-by-side viewer (PySide6)#13
longvo92 merged 6 commits into
mainfrom
claude/file-comparison-app-plan-7fdea7

Conversation

@longvo92

Copy link
Copy Markdown
Owner

Adds a Beyond-Compare-style desktop viewer on top of the existing compare core: a folder tree plus a two-pane old/new diff with a change minimap. Built in 6 phases (one commit each) so it can be reviewed / rolled back phase by phase.

Why

The tool so far only produced a static HTML report. Reviewers wanted to browse a whole codegen tree interactively and see, per file, exactly what changed old-vs-new — for .arxml, .a2l and .c/.h alike — with a minimap to spot change locations at a glance. Requested via AskUserQuestion: PySide6 native · read-only · tree left · two-pane right · minimap.

What changed

  • Phase 0 — shared view model (compare_tool/view_model.py): char_span() (intra-line changed span as plain offsets) and aligned_rows() (whole-file old/new alignment from the classified hunks). report.py now consumes char_span, so the HTML report and the viewer highlight identical characters. HTML output is byte-identical (existing report tests still pass).
  • Phase 1 — viewer skeleton (compare_tool/qtviewer/): tree.py (Qt-free folder model + status aggregation), worker.py (ScanWorker(QThread), results via signals, loud crash), app.py (MainWindow, verdict-coloured tree, red COMPARE-INCOMPLETE banner, Fusion dark palette).
  • Phase 2 — two-pane diff: two synced read-only editors with per-side line-number gutters; equal block counts make scrolling a straight mirror. Row backgrounds follow the report palette; inline spans use char_span. Identical/added/deleted/binary/error render as a message page; render failures stay loud.
  • Phase 3 — minimap: one column covering both panes, one bar per changed row coloured by kind, a viewport box, click/drag to jump.
  • Phase 4 — polish/parity: F7/F8 prev/next real change (noise skipped), tree path filter + Identical/Unimportant toggles (default off = hide noise), per-file AUTOSAR/A2L rollup in the header, moved-line count.
  • Phase 5 — packaging (packaging/): PyInstaller onefile spec (excludes unused PySide6 addons → ~45 MB) + Windows build script, producing CodeGenCompareViewer.exe.

Notes for reviewers

  • Launch: python -m compare_tool --qt [old] [new]. PySide6 is imported only under --qt (deferred like tkinter), so the CLI, tkinter GUI and HTML report keep working on a headless box with no Qt. Declared as an optional [viewer] extra.
  • Fail-safe is unchanged everywhere: an uncompared path → red banner, a scan crash → loud failure, a per-file render failure → "treat as changed" message — never an empty, clean-looking result.
  • New tests are Qt-free (test_view_model.py, test_qtviewer.py exercise the pure logic), so CI needs no display. 175 tests pass. The Qt UI was verified with offscreen smoke runs (equal block counts + scroll mirror, minimap marks, filter/nav/semantics, frozen-exe launch + scan).
  • dist//build/ stay gitignored — the exe is not committed; run packaging\build-viewer.ps1 to produce it.

🤖 Generated with Claude Code

longvo92 and others added 6 commits July 23, 2026 22:06
Add compare_tool/view_model.py with two renderer-agnostic primitives that
back the upcoming Qt two-pane viewer:

- char_span(): intra-line changed span as plain character offsets (one
  contiguous span per side, common prefix/suffix excluded).
- aligned_rows(): whole-file old/new alignment from classified hunks, with
  the shorter side of each changed block padded so the panes stay row-for-row
  aligned -- the natural Beyond-Compare model.

report.py's _char_diff now consumes char_span, so the HTML report and the Qt
viewer highlight the exact same characters and can never drift apart. HTML
output is unchanged (existing report tests still pass).

Add tests/test_view_model.py covering char_span offsets, agreement with the
report highlighter, and aligned_rows padding/lockstep/monotonic-numbering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add compare_tool/qtviewer, a PySide6 side-by-side compare app launched with
`python -m compare_tool --qt [old] [new]`. This phase lands the shell:

- tree.py: Qt-free folder-tree model (build_nodes + folder-status aggregation,
  error outranks all), unit-tested headless without PySide6.
- worker.py: ScanWorker(QThread) runs scan off the GUI thread, results cross
  back via signals only. A worker crash emits a loud failure.
- app.py: MainWindow with a QSplitter — folder tree left (verdict-coloured,
  matching the report's vocabulary) and a diff pane right. Uncompared paths
  raise a red COMPARE-INCOMPLETE banner; a Fusion dark palette matches the
  report identity. Selecting a file wires through to the diff pane.
- diffpane.py: Phase-1 stub with the stable show_file/clear seam that the
  real two-pane view replaces in Phase 2.

PySide6 is imported lazily (only under --qt), so the CLI and HTML report keep
working on a headless box. Declared as an optional [viewer] extra; qtviewer
added to the packaged modules.

Add tests/test_qtviewer.py for the tree model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the DiffPane stub with the real Beyond-Compare-style side-by-side
view, built on the shared aligned_rows model:

- DiffEditor: read-only monospace pane with a per-side line-number gutter that
  shows each row's ORIGINAL file line number (blank on padded rows), no wrap.
- DiffPane: two DiffEditors in a splitter. Because aligned_rows emits one row
  per aligned line, both editors hold the same block count, so vertical and
  horizontal scrolling mirror across the panes with a simple reentrancy-guarded
  scrollbar link.
- Row backgrounds follow the report palette (real red/green, minor yellow,
  moved blue, absent side dim filler); inline changed spans are highlighted at
  char_span offsets, marking the exact characters the HTML report marks.
- A QStackedWidget switches to a message page for identical / added / deleted /
  binary / error files (added shows new content, deleted shows old). Render is
  wrapped fail-safe: a re-read failure shows a loud "treat as changed" message
  instead of an empty (unchanged-looking) diff.
- On open, the view jumps to the first change.

Verified offscreen: equal block counts, correct row modes, scroll mirror.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add compare_tool/qtviewer/minimap.py: a thin column down the right edge of the
diff pane that compresses the whole file to one bar per changed row, coloured
by mode (real red, minor yellow, moved blue). A translucent box tracks the
visible viewport and click/drag jumps there.

One minimap covers both panes: aligned_rows yields a single shared row
sequence, so row i is the same vertical position on old and new. The map reads
the old editor for its viewport and centres it to jump; the scrollbar mirror
from Phase 2 carries the new pane along. It repaints on scroll and on content
change, and clears for one-sided (added/deleted) and message views.

Verified offscreen: marks placed at the changed rows, jump drives both panes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring the Qt viewer to parity with the HTML report's richness:

- Change navigation: F7/F8 (toolbar buttons too) step to the previous/next
  real or moved change and centre it; minor noise is skipped, and the ends
  wrap. Stops are the first row of each contiguous real/moved block.
- Tree filter panel: a path search box plus "Show: Identical / Unimportant"
  toggles, both off by default so the tree opens on real changes only
  (matching the report's hide-noise default). filter_nodes is pure and
  unit-tested; empty folders collapse away and 'error' paths never hide.
- Per-file AUTOSAR / A2L rollup under the diff header (+1 port, ~1 event, …),
  reusing the semantic diffs the scanner already attaches; moved-line count
  shown in the header.

README: document --qt, the [viewer] extra, and note the viewer is the only
part that needs PySide6.

Verified offscreen: default tree hides noise, toggles/filter work, header
shows the AUTOSAR rollup, F7/F8 cycle the changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add packaging/ so the viewer can ship as a single binary to machines with no
Python:

- viewer_entry.py: GUI-only frozen entry point (opens the viewer, optional
  OLD/NEW folder args), kept separate from compare_tool.main so the build
  carries no console/CLI baggage.
- compare-viewer.spec: onefile PyInstaller spec resolving the repo root via
  SPECPATH; excludes the unused PySide6 addons (QtQuick, WebEngine, 3D,
  Charts, multimedia, …) so the binary is ~45 MB instead of 150 MB+.
- build-viewer.ps1: one-command Windows build wrapper.

README documents the standalone-exe build. Build artifacts (build/ dist/) are
already gitignored. Verified: build completes and dist/CodeGenCompareViewer.exe
launches and scans (offscreen smoke run).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@longvo92
longvo92 merged commit 4a837a7 into main Jul 23, 2026
4 checks passed
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.

1 participant