A keyboard-driven, two-pane file manager (Total Commander–style) tuned for photo workflows — taxonomic, archival, or scientific. Built in Rust on egui.
The headline trick: smart name-sync. When you've renamed
P2230437.ORF → P2230437 Guilleminea densa.ORF, F5 (copy) and F6
(move) detect that they're the same file (matching base ID + EXIF
DateTaken + size) and converge both sides to the more-specific suffix
rather than duplicating bytes.
- Two-pane TC layout with green active-pane border, drive pills, breadcrumb path bar, recent-folders dropdown, status bar, fading toasts.
- List ↔ Thumbnail grid view per pane (
Ctrl+T). Visible-only lazy decode, background thumbnail workers, persistent disk cache. - Native preview for JXL via pure-Rust
jxl-oxide. RAW (ORF/CR2/CR3/NEF/ARW/DNG/RAF/RW2) shows embedded JPEG; HEIC/AVIF same when available. - EXIF panel + rotate + zoom in
F3preview. EXIF orientation tag auto-applied to all thumbnails and previews. - Smart name-sync with taxonomic specificity:
Species > Genus > Family > None. Conflicts queue through Dialog A/B. - Batch rename (
Shift+F2) with{name}{ext}{n}{n3}{date}{base}tokens + presets. - Undo (
Ctrl+Z) for the last reversible Copy / Move / Rename actions. - Filesystem watch (
notify) auto-reloads panes on external changes. - Per-folder rating sidecar (
.fnu_rate.json). Hotkeys0..5;Ctrl+6sorts by rating. - Find duplicates (
Ctrl+D), regex filter (Ctrl+/), recursive flat view (Ctrl+Shift+R), select-by-mask (Ctrl++ / Ctrl+-), bookmarks (Ctrl+B / Ctrl+L). - Right-click context menu: Preview, Copy path, Open with system app, Reveal in Explorer, Copy/Move to other pane (smart-sync routed).
- Sync stats modal (
F9) with side-by-side thumbnails for missing / mismatched / renamed / orphan files between panes. - Sidecar-cached EXIF: datetaken + orientation persist in
.fnu_index.json; rescans skip EXIF parse when file unchanged.
cargo build --release
./target/release/filenameupdate2Requires Rust 1.78+ (edition 2021). No native toolchain dependencies — JXL decode is pure Rust.
| Key | Action |
|---|---|
↑/↓ PgUp/PgDn |
Move cursor |
Enter |
Descend dir / open file |
Backspace |
Up to parent |
Tab |
Swap active pane |
Alt+←/→ |
Folder history back/forward |
Alt+F1 / Alt+F2 |
Drive picker for left / right pane |
Ctrl+G |
Go to path |
Ctrl+B / Ctrl+L |
Add / list bookmarks |
| Key | Action |
|---|---|
Insert |
Toggle select + advance |
Space |
Toggle select + advance |
Shift+↑/↓ |
Range select |
Ctrl+A |
Select all |
Ctrl+I |
Invert |
Ctrl++ / Ctrl+- |
Select / deselect by mask |
Ctrl+D |
Find + select duplicates |
| Key | Action |
|---|---|
F2 |
Rename |
Shift+F2 |
Batch rename (token-based) |
F3 |
Preview (RAW / JXL / standard, rotatable + zoomable) |
F4 |
Edit in text editor |
F5 |
Copy → other pane (smart name-sync) |
F6 |
Move → other pane (smart name-sync) |
F8 / Del |
Recycle |
Ctrl+F8 / Shift+Del |
Hard delete |
F9 |
Sync stats modal (compare panes) |
Ctrl+Z |
Undo last action |
Ctrl+R |
Refresh |
Ctrl+E |
Open folder in OS file manager |
Ctrl+C |
Copy path |
| Key | Action |
|---|---|
R / Shift+R |
Rotate left / right |
| Mouse wheel | Zoom |
| Key | Action |
|---|---|
0 |
Clear rating |
1..5 |
Set 1–5 stars |
Ctrl+6 |
Sort by rating |
| Key | Action |
|---|---|
Ctrl+T |
List ↔ thumbnail grid |
Ctrl+/ |
Toggle regex filter |
Ctrl+Shift+R |
Toggle flat (recursive) view |
Ctrl+H |
Toggle hidden files |
Ctrl+1..5 |
Sort by name / size / mtime / datetaken / base ID |
F1 |
Help overlay |
Ctrl+Q |
Quit |
For each source file, the planner builds an index of the target side keyed by filename and base ID, then classifies each pair:
- Same name, same content →
NoOp(true sync). - Same content, different suffix — same base ID + size + DateTaken, different name (e.g. left
P2230437.ORF, rightP2230437 Guilleminea densa.ORF):- Parses each side's suffix into
None < Family < Genus < Speciesranks. - Renames the lower-rank side to the higher-rank name.
- Tie → Dialog B (
Use Left name | Use Right name | Auto: more-specific | Skip).
- Parses each side's suffix into
- Same name, different content → Dialog A (
Keep Left | Keep Right | Keep Both with _2 suffix | Skip). - No match → plain copy or move.
Move actions additionally recycle the (possibly renamed) source after the rename converges names.
When the cursor is on a subdirectory with no file selection, F5/F6 operate on the folder:
plan_folderflags base-ID conflicts (same base ID, different names between source folder and same-named target folder) and missing-in-source files (target has files source doesn't).- Anomalies surface in a confirm dialog.
- On confirm, the planner re-plans per file with
plan_sync, so twin-name files still converge under the more-specific suffix instead of erroring on collision.
A .fnu_index.json sidecar is written to each folder on every op. When you delete files manually in one pane, the tool diffs the on-disk state against the prior index and can propose matching deletions in the other pane.
src/
├── app.rs # State, frame tick, dialog wiring
├── pane.rs # PaneState: cursor, selection, sort, filter, view mode
├── ops.rs # Step (Copy/Move/Rename/Recycle/...), plan_sync, plan_folder
├── matching.rs # TargetIndex + MatchKind classifier
├── parse.rs # Filename → (base_id, suffix, specificity)
├── conflict.rs # ConflictQueue for Dialog A/B routing
├── scan.rs # Parallel directory scan (rayon)
├── decode.rs # Unified decode: JXL (jxl-oxide), RAW embedded JPEG, EXIF rotate
├── thumb_worker.rs # Background decode pool (crossbeam-channel)
├── thumb_cache.rs # Disk thumb cache (%LOCALAPPDATA%/filenameupdate2/thumbs)
├── fs_watch.rs # notify-based pane auto-reload
├── undo.rs # Reversible Step inverse + capped stack
├── toast.rs # Fading status toast queue
├── ratings.rs # .fnu_rate.json sidecar
├── exif.rs # EXIF DateTaken, Orientation, summary
├── index.rs # .fnu_index.json sidecar with cached EXIF
├── config.rs # Persisted per-pane settings, bookmarks, recent
├── ui/
│ ├── panes.rs # List + grid view rendering
│ ├── dialog.rs # All modals (conflict, bulk-delete, preview, batch-rename, sync-stats, help)
│ ├── toolbar.rs # Top button bar
│ ├── statusbar.rs # Bottom status + toast stack
│ └── theme.rs # Dark theme colors
└── lib.rs / main.rs
cargo test --tests # 44 integration tests across 5 suites
cargo test --lib # 56 unit testsUI integration tests drive App::tick(&egui::Context) directly with synthetic RawInput. Snapshot tests hash tessellated clip-rects to catch layout drift. End-to-end name-sync resolution is covered by tests/ui_integration.rs::f5_copy_resolves_twin_name_via_specificity.
| Format | Thumbnail | Preview | Notes |
|---|---|---|---|
| JPEG / PNG / GIF / WebP / BMP / TIFF | ✅ | ✅ | egui native loaders |
| JXL | ✅ | ✅ | pure Rust via jxl-oxide |
| ORF / CR2 / CR3 / NEF / ARW / DNG / RAF / RW2 | ✅ | ✅ | embedded JPEG extracted from RAW |
| HEIC / HEIF / AVIF | embedded JPEG only (pure-Rust HEVC/AV1 decode not yet viable) |
MIT.