No description
Find a file
Cyryl Płotnicki ddb4d35a4d
All checks were successful
build / test (push) Successful in 1m2s
build / check-nix (push) Successful in 1m22s
update docs for tests
2025-12-30 09:23:50 +00:00
.forgejo/workflows project init 2025-12-28 19:32:13 +00:00
src scrolling and vidual fixes 2025-12-29 22:04:00 +00:00
tests move to normal tagged tests 2025-12-29 23:10:13 +00:00
.envrc project init 2025-12-28 19:32:13 +00:00
.gitignore project init 2025-12-28 19:32:13 +00:00
build.rs sort tests 2025-12-29 22:39:13 +00:00
Cargo.lock move to normal tagged tests 2025-12-29 23:10:13 +00:00
Cargo.toml move to normal tagged tests 2025-12-29 23:10:13 +00:00
CLAUDE.md walking skeleton 2025-12-28 21:03:13 +00:00
flake.lock project init 2025-12-28 19:32:13 +00:00
flake.nix project init 2025-12-28 19:32:13 +00:00
LICENSE Initial commit 2025-12-28 19:09:46 +00:00
PLAN.md update docs for tests 2025-12-30 09:23:50 +00:00
README.md refactor to tests being generated from data 2025-12-29 12:03:19 +00:00
spec.md update docs for tests 2025-12-30 09:23:50 +00:00

deescalate

A TUI tool for resolving file sync conflicts, with a focus on Syncthing.

Note: This project is under active development. See PLAN.md for current progress. The documentation below describes the planned feature set.

What It Does

When file synchronization tools like Syncthing detect that a file was modified on multiple devices simultaneously, they create conflict copies rather than silently overwriting changes. This leaves you with files like:

notes.md
notes.sync-conflict-20240115-143052-5CYLJKE.md

deescalate helps you resolve these conflicts by:

  1. Scanning directories recursively for conflict files
  2. Analyzing both versions using file-type-specific metadata
  3. Suggesting automatic resolution when one file is clearly newer or identical
  4. Showing diffs when manual decision is needed
  5. Confirming before making any disk changes

Installation

From crates.io

cargo install deescalate

With Nix

nix run github:cyplo/deescalate

Or add to your flake inputs:

{
  inputs.deescalate.url = "github:cyplo/deescalate";
}

Usage

deescalate <DIRECTORY>

Scan a directory recursively for conflicts and open the TUI:

deescalate ~/Sync

TUI Controls

Key Action
j/k or ↑/↓ Navigate list
Enter Open conflict details
Space Toggle selection
a Auto-resolve selected
d Show diff
c Commit changes
q Quit
? Help

Supported File Types

Type Extensions Metadata Used Diff Support
Text .txt, .md, .json, .yaml, .xml, etc. Line count, content hash Full text diff
Images .jpg, .jpeg, .png EXIF dates, dimensions Metadata comparison
Word .docx, .doc Author, modified date, revision diffoscope
Excel .xlsx, .xls Author, modified date diffoscope
LibreOffice .odt, .ods Author, modified date, edit cycles diffoscope
PDF .pdf Author, creation/mod dates diffoscope
Unknown Any other extension mtime, size, content hash None (metadata only)

Unknown file types are handled gracefully using filesystem metadata. You can still resolve conflicts based on modification times, file sizes, and whether the files are identical (via hash comparison).

How Resolution Works

Automatic Resolution

deescalate suggests automatic resolution when:

  • One file is clearly newer (modification time differs by >1 hour)
  • Files are identical (same content hash)
  • One file is empty (keep the non-empty one)

You always confirm before any disk changes are made.

Manual Resolution

When automatic resolution isn't confident, you see:

  • Side-by-side metadata comparison
  • Full diff view (text files) or structural diff (binary files via diffoscope)
  • Options: keep original, keep conflict, keep both, skip

Optional Dependencies

For enhanced diff support:

  • difftastic - Syntax-aware diffs for source code
  • diffoscope - Deep comparison of binary files (Office docs, PDFs, images)

Without these, deescalate falls back to basic metadata comparison.

Development

Prerequisites

  • Rust 1.75+ (uses edition 2024)
  • Nix (optional, for reproducible dev environment)

Setup

# With Nix
nix develop

# Or manually
cargo build

Running Tests

This project always uses cargo-nextest for testing. Do not use cargo test.

cargo nextest run

Test Architecture

Tests are data-driven and auto-generated from the tests/test_data/ directory structure:

tests/test_data/
├── text/                    # Text file conflict scenarios
│   ├── simple/              # Basic conflict detection
│   ├── multiple/            # Multiple conflicts for same file
│   ├── newer_original/      # Auto-resolution by mtime
│   └── identical/           # Identical content detection
├── ui/                      # UI/display tests
│   └── help_text/           # Help instructions visible
└── nested/                  # Nested directory handling

Each test case directory contains:

  • Conflict files - The actual files to test (original + conflict versions)
  • expected.toml - Declares what the test should verify

Example expected.toml:

name = "Text file with newer original"
description = "Original is 2 hours newer - should suggest keeping it"

[scan]
conflict_groups = 1

[setup]
# Set file modification times (hours ago)
mtime_hours_ago = { "notes.md" = 0, "notes.sync-conflict-....md" = 2 }

[display]
must_contain = ["notes.md", "1 conflict"]
must_contain_any = [["keep original", "Keep original"]]

To add a new test case: Create a directory under tests/test_data/ with test files and an expected.toml. The test is automatically discovered and run.

Project Structure (Target Architecture)

src/
├── main.rs              # Entry point, CLI
├── lib.rs               # Library root
├── scanner.rs           # Find conflicts by pattern
├── types.rs             # Core types
├── filetypes/           # File-type handlers (colocated logic)
│   ├── mod.rs           # FileType trait, registry
│   ├── text.rs          # Text: metadata, diff, resolution
│   ├── image.rs         # Images: EXIF, metadata display
│   ├── office.rs        # Office docs: OOXML/ODF metadata
│   ├── pdf.rs           # PDF metadata
│   └── unknown.rs       # Fallback: mtime, size, hash only
├── resolver.rs          # Resolution logic
├── tui/                 # Terminal UI
│   ├── app.rs           # App state
│   ├── scan_view.rs     # Conflict list
│   ├── detail_view.rs   # Diff/metadata view
│   └── confirm_view.rs  # Confirmation
└── operations.rs        # File operations

See PLAN.md for implementation progress.

Adding a New File Type

  1. Create src/filetypes/yourtype.rs
  2. Implement the FileType trait:
    • matches() - detect files by extension
    • extract_metadata() - get comparison data
    • suggest_resolution() - auto-resolve if confident
    • generate_diff() - produce diff output
    • render_details() - TUI display
  3. Register in src/filetypes/mod.rs

All file-type logic is colocated in one file for easy understanding and modification.

Syncthing Conflict Format

Syncthing creates conflict files with this naming pattern:

<filename>.sync-conflict-<YYYYMMDD>-<HHMMSS>-<DEVICEID>.<ext>
  • Date and time indicate when the conflict was detected
  • Device ID (7 chars) identifies which device created the conflicting version

License

AGPL-3.0

Contributing

Contributions welcome! Please read the spec.md for detailed requirements before implementing features.