Skip to content
TwinScope0.3.10

Folders

Two recursive scans, then one decision per path. Per-file status with a glyph as well as a colour, renames paired, and any file pair one double-click away — with the tree still there when you come back.

GIF · press play
Drill in, and back. Returning does not re-scan — the parent comparison was kept whole.

Two passes, not one#

The engine walks each side into a flat map of relative paths first, and only then decides a status per path.

Keeping the walk and the comparison separate is what makes "one side could not be read" a per-entry problem instead of an aborted scan. A folder containing one protected subdirectory still produces a useful comparison: that subdirectory becomes a single error row, and everything else is compared normally.

Per-file status#

StatusGlyphMeaning
AddedPresent only on the after side
DeletedPresent only on the before side
ModifiedPresent on both sides, different content
Identical·Present on both sides, same content
Unreadable!One side could not be read; the row carries the reason

Rows are aligned pairs rather than two independent trees: a file that exists on one side only lines up against a visible gap, which is the only way the eye can tell what happened. Directories inherit their subtree — a folder counts as modified when anything inside it is, so a collapsed tree still tells you where to look.

Click to enlarge
One row per path, two stripes. An empty stripe is a file that side does not have.

How "modified" is decided#

Cheapest check first, and content is only read when nothing cheaper can answer:

  1. Sizes differ → modified. One stat per side settles it.
  2. Same size, same timestamp → identical.
  3. Same size, different timestamp → the only ambiguous case, and the only one that costs a read: both sides are hashed with SHA-256 and compared.

That ordering is why a ten-thousand-file tree stays fast. The notes for the comparison record how many files actually needed hashing, so the cost is visible rather than mysterious.

Renames#

An unmatched deletion and an unmatched addition are often one file that moved. They pair up: both rows keep their status, each gains a note — renamed from old-name on one side, renamed to new-name on the other — and the count appears in the summary strip. Scoring uses three signals, in falling order of certainty:

  1. Identical content. A hash match is not a guess.
  2. The same basename elsewhere. src/a.tssrc/lib/a.ts is a move even when the content also changed — so a cross-directory rename is found, not just one within a folder.
  3. Similar content. Chunk hashes over a sample, plus size proximity, for a file that was renamed and edited.

Filters#

Two filters, and they compose:

  • Status — All, Added, Deleted, Modified, Identical.
  • Name — the toolbar box. *.ts behaves like a shell glob (* and ? both work); anything without a wildcard is a plain case-insensitive substring, because that is what typing a few letters into a filter box means.

A directory survives whenever any of its children does, so filtering never leaves a file floating without its folder. When nothing survives, the pane says "No files match this filter" rather than going blank.

Click to enlarge
Status and name filter together. The tree keeps its spine either way.

Drill in to any file pair#

Double-click a row — or select it and press — to open that one file pair as its own comparison, detected and routed like any other. Only files present on both sides can be opened; there is nothing to compare a deletion against.

The toolbar then shows a breadcrumb back to the folder comparison. Pressing it restores the tree without re-running anything: the parent comparison is stashed whole, because a ten-thousand-file scan must not repeat just because someone opened a file.

Click to enlarge
The breadcrumb is the whole mechanism. One click each way, no second scan.

What is skipped, and said#

SkippedWhy
.git, node_modules, .DS_StoreThe ignore list. Nobody is comparing two object stores by accident
SymlinksCounted and skipped rather than followed: following them risks a cycle, and comparing a link target is a different question from comparing files

Both are named in the notes for the comparison — including the number of symlinks skipped — and those notes travel into any report you export.

Limits#

LimitValueWhat happens past it
Entries per side50,000The scan stops, the summary is marked a partial scan, and a note says so
Depth40 levelsThe walk stops descending and records it — a tree this deep is generated, or a symlink loop we failed to spot

Alongside the added / removed / modified counts, the strip carries this engine's own vocabulary: identical files, renames, unreadable entries, and the net size change across the whole tree.